calcom/cal.diy · error · Error

App is already installed

Error message

App is already installed

What it means

Thrown by defaultIntegrationAddHandler when an app that does not support multiple installs already has a credential row for the same appId and user (or user+team). It is a plain Error (not HttpError), so it bubbles into the route's catch block and is normalized by getServerErrorFromUnknown before being sent to the client. This prevents duplicate OAuth/credential rows for single-install apps.

Source

Thrown at apps/web/pages/api/integrations/[...args].ts:38

  slug: string;
  supportsMultipleInstalls: boolean;
  appType: string;
  user?: Session["user"];
  teamId?: number;
  createCredential: AppDeclarativeHandler["createCredential"];
}) => {
  if (!user?.id) {
    throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
  }
  if (!supportsMultipleInstalls) {
    const alreadyInstalled = await prisma.credential.findFirst({
      where: {
        appId: slug,
        ...(teamId ? { AND: [{ userId: user.id }, { teamId }] } : { userId: user.id }),
      },
    });
    if (alreadyInstalled) {
      throw new Error("App is already installed");
    }
  }

  await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id });

  await createCredential({ user: user, appType, slug, teamId });
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  // Check that user is authenticated
  req.session = await getServerSession({ req });

  const { args, teamId } = req.query;

  if (!Array.isArray(args)) {
    return res.status(404).json({ message: `API route not found` });
  }

View on GitHub (pinned to 176037d0af)

Solutions

  1. Check the Credential table for an existing row with that appId + userId (or teamId) and uninstall it first if reinstall is intended.
  2. Refresh the integrations page so the UI shows the app as installed rather than offering install again.
  3. If the app should allow multiple installs, set supportsMultipleInstalls: true in the app's package config.
  4. Delete the orphaned credential row via the uninstall flow or directly in the DB, then retry.

Example fix

// before: installing again without checking
await installApp('zoom');
// after: guard with the provided helper
import { isAppInstalled } from '@calcom/app-store/_utils/installation';
if (!(await isAppInstalled({ appId: 'zoom', userId }))) {
  await installApp('zoom');
}
Defensive patterns

Strategy: validation

Validate before calling

import { isAppInstalled } from '@calcom/app-store/_utils/installation';
const alreadyInstalled = await isAppInstalled({ appId: slug, userId: user.id });
if (alreadyInstalled) {
  return res.status(200).json({ message: 'Already installed', alreadyInstalled: true });
}

Type guard

null

Try / catch

try {
  await defaultIntegrationAddHandler({ ... });
} catch (e) {
  if (e instanceof Error && e.message === 'App is already installed') {
    return res.status(409).json({ message: e.message });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling /api/integrations/<single-install-app>/add for a user who already has a Credential row with appId === slug (userId-scoped), or with both userId and teamId matching when a teamId query param is present. Common during repeated install button clicks or after a partially completed install left a credential behind.

Common situations: User clicked install twice; a previous OAuth redirect already created the credential but the UI did not refresh; stale credential row left after an uninstall that partially failed; testing installs repeatedly without cleaning the credential table.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/1d9c783b76a18a0b. Report an issue: GitHub.