{"record":{"id":"b68a8e2e3c548b6e","repo":"toeverything/AFFiNE","slug":"license-not-found","errorCode":"license_not_found","errorMessage":"License not found.","messagePattern":"License not found\\.","errorType":"exception","errorClass":"LicenseNotFound","httpStatus":404,"severity":"error","filePath":"packages/backend/server/src/plugins/license/service.ts","lineNumber":250,"sourceCode":"    });\n    this.event.emit('workspace.subscription.activated', {\n      workspaceId,\n      plan: data.plan as SubscriptionPlan,\n      recurring: data.recurring as SubscriptionRecurring,\n      quantity: data.quantity,\n    });\n    return installed;\n  }\n\n  async removeTeamLicense(workspaceId: string) {\n    const license = await this.db.installedLicense.findUnique({\n      where: {\n        workspaceId,\n      },\n    });\n\n    if (!license) {\n      throw new LicenseNotFound();\n    }\n\n    await this.db.installedLicense.deleteMany({\n      where: {\n        workspaceId: license.workspaceId,\n      },\n    });\n    await this.entitlement.revokeBySubject('selfhost_license', license.key);\n\n    if (license.variant !== SubscriptionVariant.Onetime) {\n      await this.deactivateTeamLicense(license);\n    }\n\n    this.event.emit('workspace.subscription.canceled', {\n      workspaceId: license.workspaceId,\n      plan: SubscriptionPlan.SelfHostedTeam,\n      recurring: license.recurring as SubscriptionRecurring,\n    });","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/license/service.ts#L232-L268","documentation":"removeTeamLicense() deletes the installedLicense rows for a workspace and revokes the matching selfhost_license entitlement. It throws LicenseNotFound when no installedLicense row exists for that workspaceId, so there is nothing to remove or deactivate. This keeps the remove flow from emitting revoke/deactivate events for a phantom license.","triggerScenarios":"Calling removeTeamLicense(workspaceId) when the workspace never activated a license, or after it was already removed; concurrent double-click on 'remove license' in the admin UI where the first request already deleted the row.","commonSituations":"Non-idempotent retry after a timed-out request that actually succeeded; UI state showing a license that a background job (e.g. revalidation emitting workspace.subscription.canceled) already removed; scripts that run cleanup on fresh workspaces.","solutions":["Treat it as success if your goal is 'no license present': re-check with getLicense(workspaceId) and proceed only when a row exists.","Guard the UI against double-submits while removal is in flight.","For scripted flows, make removal conditional: fetch first, then remove."],"exampleFix":"// before\nawait licenseService.removeTeamLicense(workspaceId);\n\n// after\nif (await licenseService.getLicense(workspaceId)) {\n  await licenseService.removeTeamLicense(workspaceId);\n}","handlingStrategy":"validation","validationCode":"if (await licenseService.getLicense(workspaceId)) {\n  await licenseService.removeTeamLicense(workspaceId);\n} else {\n  // nothing to remove — treat as success\n}","typeGuard":"function isLicenseNotFound(e: unknown): e is LicenseNotFound {\n  return e instanceof LicenseNotFound;\n}","tryCatchPattern":"try {\n  await licenseService.removeTeamLicense(workspaceId);\n} catch (e) {\n  if (e instanceof LicenseNotFound) return; // idempotent: already removed\n  throw e;\n}","preventionTips":["Make removal conditional on a prior getLicense check.","Guard UI double-submits while removal is in flight.","Treat LicenseNotFound on remove as the desired end state."],"tags":["license","self-hosted","not-found","idempotency"],"backgroundTag":"license-not-found","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}