toeverything/AFFiNE · error · BadRequest
bad_request
bad_request
Error message
Self-hosted commercial entitlements require a signed license.
What it means
BadRequest thrown by upsertAdminGrant when env.selfhosted is true. Self-hosted deployments are not permitted to mint commercial entitlements (pro/team/ai) via the admin grant API; they must use a signed license instead. This is an intentional gate, correctly typed as BadRequest with code=bad_request.
Source
Thrown at packages/backend/server/src/core/entitlement/service.ts:213
return updated;
}
const created = await this.db.entitlement.create({ data });
if (emit) {
await this.emitEntitlementChanged(created);
}
return created;
}
async upsertAdminGrant(input: {
targetType: Exclude<TargetType, 'instance'>;
targetId: string;
plan: string;
quantity?: number | null;
}) {
this.assertAdminGrantInput(input.targetType, input.plan);
if (env.selfhosted) {
throw new BadRequest(
'Self-hosted commercial entitlements require a signed license.'
);
}
const quantity =
input.targetType === 'workspace'
? this.normalizedQuantity(input.quantity)
: undefined;
resolveEntitlementV1({
deploymentType: 'cloud',
targetType: input.targetType,
targetId: input.targetId,
plan: input.plan,
quantity,
now: new Date().toISOString(),
});
const subjectId = this.adminGrantSubjectId(
input.targetType,View on GitHub (pinned to 26c515e050)
Solutions
- On self-hosted, use the license-upload flow (resolveEntitlementV1 with a signed license) instead of admin grants.
- If you actually intend cloud behavior, verify env.selfhosted is false on this node and that you're hitting the right deployment.
- Update admin tooling to detect selfhosted and skip/warn on the grant path rather than erroring.
- Document the divergence in the admin panel so operators don't attempt the unsupported call.
Example fix
// before
if (env.selfhosted) {
throw new BadRequest('Self-hosted commercial entitlements require a signed license.');
}
// after — the caller side: branch before calling
if (env.selfhosted) {
return uploadSelfHostLicense({ workspaceId, license });
}
return service.upsertAdminGrant(input); Defensive patterns
Strategy: validation
Validate before calling
function assertCanAdminGrant(env) {
if (env.selfhosted) {
throw new UserError('Use the license-upload flow on self-hosted, not admin grants');
}
} Type guard
function isSelfHostLicenseRequired(e: unknown): boolean {
return e instanceof Error && e.message === 'Self-hosted commercial entitlements require a signed license.';
} Try / catch
if (!env.selfhosted) {
await entitlement.upsertAdminGrant(input);
} else {
await entitlement.upsertSelfHostLicense({ workspaceId, license });
} Prevention
- Branch admin tooling on deployment type before calling grant APIs.
- Document that self-hosted uses licenses, not grants.
- Verify env.selfhosted is set correctly per node.
When it happens
Trigger: An admin tool or script calls the upsertAdminGrant GraphQL mutation (or the underlying service) on a self-hosted instance. The same call works on cloud but is blocked on selfhosted by design.
Common situations: Operators trying to replicate a cloud admin workflow on self-hosted. A shared admin script run against the wrong deployment type. Misconfigured env.selfhosted flag (accidentally true on a cloud node).
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/7be40ddfbf9c4af5.
Report an issue: GitHub.