toeverything/AFFiNE · warning · BadRequest
bad_request
bad_request
Error message
Self-hosted commercial entitlements require a signed license.
What it means
EntitlementService.upsertAdminGrant creates commercial plan grants (pro, lifetime_pro, ai, team) and is cloud-only: when env.selfhosted is true it throws BadRequest ('Self-hosted commercial entitlements require a signed license.'). Self-hosted deployments must obtain paid features through an activated signed license key, not manual admin grants.
Solutions
- On self-hosted, activate a signed license via the license activation API instead of admin grants
- If this environment is genuinely cloud, check why env.selfhosted is set (AFFI_NE env/selfhosted config) and correct it
- Use only free/built-in features on unlicensed self-hosted installs
- Deploy the cloud edition if programmatic admin grants are a hard requirement
Example fix
// before
await entitlement.upsertAdminGrant({ targetType: 'user', targetId, plan: 'pro' });
// after
if (env.selfhosted) {
await licenseService.activate(signedLicenseKey); // commercial features come from the license
} else {
await entitlement.upsertAdminGrant({ targetType: 'user', targetId, plan: 'pro' });
} Defensive patterns
Strategy: validation
Validate before calling
if (env.selfhosted) {
throw new Error('use license activation for commercial plans on self-hosted');
}
await entitlement.upsertAdminGrant({ targetType, targetId, plan }); Type guard
function isSelfhostedLicenseRequired(e: unknown): boolean {
const err = e as { extensions?: { code?: string }; message?: string };
return err.extensions?.code === 'bad_request' && /signed license/.test(err.message ?? '');
} Try / catch
try {
await upsertAdminGrant(input);
} catch (e) {
if (isSelfhostedLicenseRequired(e)) {
return activateLicenseInstead(); // self-hosted path
}
throw e;
} Prevention
- Gate admin-grant tooling on deployment type (cloud only)
- Automate license activation for self-hosted installs that need paid plans
- Log the effective env.selfhosted value at startup to catch misconfiguration
When it happens
Trigger: Calling the admin GraphQL mutation that reaches upsertAdminGrant (user/workspace plan grants from the admin panel) on a self-hosted build; running admin tooling written for the cloud edition against a self-hosted deployment.
Common situations: Self-hosters trying to enable team/pro features by hand; scripts copied from cloud ops playbooks; dev machines where the selfhosted env flag is unexpectedly set.
Understand the failure class
Background: BAD_REQUEST error code: request rejected as invalid (HTTP 400) - causes and fixes across libraries — this error's family across 8 libraries.
Related errors
- license_expired
- invalid_license_to_activate
- invalid_license_to_activate
- license_not_found
- license_not_found
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/7be40ddfbf9c4af5.
Report an issue: GitHub.
Appendix: 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 b4c8548c09)