toeverything/AFFiNE · error · LicenseNotFound
license_not_found
license_not_found
Error message
License not found.
What it means
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.
Source
Thrown at packages/backend/server/src/plugins/license/service.ts:250
});
this.event.emit('workspace.subscription.activated', {
workspaceId,
plan: data.plan as SubscriptionPlan,
recurring: data.recurring as SubscriptionRecurring,
quantity: data.quantity,
});
return installed;
}
async removeTeamLicense(workspaceId: string) {
const license = await this.db.installedLicense.findUnique({
where: {
workspaceId,
},
});
if (!license) {
throw new LicenseNotFound();
}
await this.db.installedLicense.deleteMany({
where: {
workspaceId: license.workspaceId,
},
});
await this.entitlement.revokeBySubject('selfhost_license', license.key);
if (license.variant !== SubscriptionVariant.Onetime) {
await this.deactivateTeamLicense(license);
}
this.event.emit('workspace.subscription.canceled', {
workspaceId: license.workspaceId,
plan: SubscriptionPlan.SelfHostedTeam,
recurring: license.recurring as SubscriptionRecurring,
});View on GitHub (pinned to b4c8548c09)
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.
Example fix
// before
await licenseService.removeTeamLicense(workspaceId);
// after
if (await licenseService.getLicense(workspaceId)) {
await licenseService.removeTeamLicense(workspaceId);
} Defensive patterns
Strategy: validation
Validate before calling
if (await licenseService.getLicense(workspaceId)) {
await licenseService.removeTeamLicense(workspaceId);
} else {
// nothing to remove — treat as success
} Type guard
function isLicenseNotFound(e: unknown): e is LicenseNotFound {
return e instanceof LicenseNotFound;
} Try / catch
try {
await licenseService.removeTeamLicense(workspaceId);
} catch (e) {
if (e instanceof LicenseNotFound) return; // idempotent: already removed
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/b68a8e2e3c548b6e.
Report an issue: GitHub.