{"record":{"id":"9e9bbdc4d0066c95","repo":"RocketChat/Rocket.Chat","slug":"app-already-exists","errorCode":null,"errorMessage":"App already exists.","messagePattern":"App already exists\\.","errorType":"exception","errorClass":null,"httpStatus":400,"severity":"error","filePath":"apps/meteor/ee/server/apps/storage/AppRealStorage.ts","lineNumber":23,"sourceCode":"import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';\nimport type { ISetting } from '@rocket.chat/apps-engine/definition/settings';\nimport type { Apps } from '@rocket.chat/models';\nimport { removeEmpty } from '@rocket.chat/tools';\nimport type { UpdateFilter } from 'mongodb';\n\nexport class AppRealStorage extends AppMetadataStorage {\n\tconstructor(private db: typeof Apps) {\n\t\tsuper('mongodb');\n\t}\n\n\tpublic async create(item: IAppStorageItem): Promise<IAppStorageItem> {\n\t\titem.createdAt = new Date();\n\t\titem.updatedAt = new Date();\n\n\t\tconst doc = await this.db.findOne({ $or: [{ id: item.id }, { 'info.nameSlug': item.info.nameSlug }] });\n\n\t\tif (doc) {\n\t\t\tthrow new Error('App already exists.');\n\t\t}\n\n\t\tconst nonEmptyItem = removeEmpty(item);\n\t\tconst id = (await this.db.insertOne(nonEmptyItem)).insertedId as unknown as string;\n\t\tnonEmptyItem._id = id;\n\n\t\treturn nonEmptyItem;\n\t}\n\n\tpublic async retrieveOne(id: string): Promise<IAppStorageItem> {\n\t\treturn this.db.findOne({ $or: [{ _id: id }, { id }] });\n\t}\n\n\tpublic async retrieveAll(): Promise<Map<string, IAppStorageItem>> {\n\t\tconst docs = await this.db.find({}).toArray();\n\t\tconst items = new Map();\n\n\t\tdocs.forEach((i) => items.set(i.id, i));","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/2a7de457074cbb4d4373fbd9a4e5bea292c9c764/apps/meteor/ee/server/apps/storage/AppRealStorage.ts#L5-L41","documentation":"Thrown by AppRealStorage.create when inserting a new app record would collide with an existing one on either id or info.nameSlug (a $or lookup runs before insert). The storage layer enforces uniqueness of the app ID and the URL-friendly app name so two installs of the same app (or two apps with the same slug) cannot coexist. It is a conflict error: the app is already installed or another app occupies the slug.","triggerScenarios":"Installing an app whose id already exists in the apps collection, or whose nameSlug matches a different installed app (e.g. two builds of similarly named apps); concurrent installs of the same app racing past the findOne; reinstalling an app that was removed but whose DB document remained.","commonSituations":"Reinstalling an app that is actually still installed (stale UI state); installing a fork with the same name (slug clash); app removed outside the normal uninstall path leaving orphan docs; double-click/double-submit on install.","solutions":["Check installed apps (Administration -> Apps) — if present, use update/re-enable instead of install.","Fully uninstall the existing app (which removes its storage doc) and retry the install.","Slug clash with a different app: uninstall the other app or rebuild your fork with a distinct app name so nameSlug differs.","Orphaned doc: remove the stale document from the apps collection (support-guided) then reinstall."],"exampleFix":"// before\nawait storage.create(item); // \"App already exists.\" when id/nameSlug collides\n\n// after: check before create\nconst existing = await storage.retrieveOne(item.id);\nif (existing) {\n  await storage.update(item.id, item); // in-place update path\n} else {\n  await storage.create(item);\n}","handlingStrategy":"validation","validationCode":"// Check-before-create (mirrors the storage guard)\nconst existing = await storage.retrieveOne(item.id);\nconst slugTaken = await storage.internalSearchByIds?; // or query by info.nameSlug\nif (existing || slugTaken) throw new ConflictError('app-installed');","typeGuard":null,"tryCatchPattern":"try {\n  await storage.create(item);\n} catch (e) {\n  if (e instanceof Error && e.message === 'App already exists.') {\n    await storage.update(item.id, item); // converge via update instead of failing the install\n  }\n}","preventionTips":["Idempotent install flows: retrieve-then-create/update instead of blind create.","Disable the install button while an install request is in flight to prevent double submits.","Ensure forks use unique app names so nameSlug never collides with marketplace apps."],"tags":["apps-engine","storage","duplicate","install","mongodb"],"backgroundTag":"duplicate-resource-conflict","analyzedSha":"2a7de457074cbb4d4373fbd9a4e5bea292c9c764","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}