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

  1. Treat it as success if your goal is 'no license present': re-check with getLicense(workspaceId) and proceed only when a row exists.
  2. Guard the UI against double-submits while removal is in flight.
  3. 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

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.