{"record":{"id":"1d9c783b76a18a0b","repo":"calcom/cal.diy","slug":"app-is-already-installed","errorCode":null,"errorMessage":"App is already installed","messagePattern":"App is already installed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/web/pages/api/integrations/[...args].ts","lineNumber":38,"sourceCode":"  slug: string;\n  supportsMultipleInstalls: boolean;\n  appType: string;\n  user?: Session[\"user\"];\n  teamId?: number;\n  createCredential: AppDeclarativeHandler[\"createCredential\"];\n}) => {\n  if (!user?.id) {\n    throw new HttpError({ statusCode: 401, message: \"You must be logged in to do this\" });\n  }\n  if (!supportsMultipleInstalls) {\n    const alreadyInstalled = await prisma.credential.findFirst({\n      where: {\n        appId: slug,\n        ...(teamId ? { AND: [{ userId: user.id }, { teamId }] } : { userId: user.id }),\n      },\n    });\n    if (alreadyInstalled) {\n      throw new Error(\"App is already installed\");\n    }\n  }\n\n  await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id });\n\n  await createCredential({ user: user, appType, slug, teamId });\n};\n\nconst handler = async (req: NextApiRequest, res: NextApiResponse) => {\n  // Check that user is authenticated\n  req.session = await getServerSession({ req });\n\n  const { args, teamId } = req.query;\n\n  if (!Array.isArray(args)) {\n    return res.status(404).json({ message: `API route not found` });\n  }\n","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/pages/api/integrations/[...args].ts#L20-L56","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the Credential table for an existing row with that appId + userId (or teamId) and uninstall it first if reinstall is intended.","Refresh the integrations page so the UI shows the app as installed rather than offering install again.","If the app should allow multiple installs, set supportsMultipleInstalls: true in the app's package config.","Delete the orphaned credential row via the uninstall flow or directly in the DB, then retry."],"exampleFix":"// before: installing again without checking\nawait installApp('zoom');\n// after: guard with the provided helper\nimport { isAppInstalled } from '@calcom/app-store/_utils/installation';\nif (!(await isAppInstalled({ appId: 'zoom', userId }))) {\n  await installApp('zoom');\n}","handlingStrategy":"validation","validationCode":"import { isAppInstalled } from '@calcom/app-store/_utils/installation';\nconst alreadyInstalled = await isAppInstalled({ appId: slug, userId: user.id });\nif (alreadyInstalled) {\n  return res.status(200).json({ message: 'Already installed', alreadyInstalled: true });\n}","typeGuard":"null","tryCatchPattern":"try {\n  await defaultIntegrationAddHandler({ ... });\n} catch (e) {\n  if (e instanceof Error && e.message === 'App is already installed') {\n    return res.status(409).json({ message: e.message });\n  }\n  throw e;\n}","preventionTips":["Call isAppInstalled before invoking the install path to short-circuit duplicates.","Disable/debounce the install button in the UI after the first click.","Treat 'App is already installed' as an idempotent success in the client."],"tags":["integrations","duplicate","credentials","business-logic"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}