{"record":{"id":"7be40ddfbf9c4af5","repo":"toeverything/AFFiNE","slug":"bad-request-7be40d","errorCode":"bad_request","errorMessage":"Self-hosted commercial entitlements require a signed license.","messagePattern":"Self-hosted commercial entitlements require a signed license\\.","errorType":"validation","errorClass":"BadRequest","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/entitlement/service.ts","lineNumber":213,"sourceCode":"      return updated;\n    }\n\n    const created = await this.db.entitlement.create({ data });\n    if (emit) {\n      await this.emitEntitlementChanged(created);\n    }\n    return created;\n  }\n\n  async upsertAdminGrant(input: {\n    targetType: Exclude<TargetType, 'instance'>;\n    targetId: string;\n    plan: string;\n    quantity?: number | null;\n  }) {\n    this.assertAdminGrantInput(input.targetType, input.plan);\n    if (env.selfhosted) {\n      throw new BadRequest(\n        'Self-hosted commercial entitlements require a signed license.'\n      );\n    }\n    const quantity =\n      input.targetType === 'workspace'\n        ? this.normalizedQuantity(input.quantity)\n        : undefined;\n    resolveEntitlementV1({\n      deploymentType: 'cloud',\n      targetType: input.targetType,\n      targetId: input.targetId,\n      plan: input.plan,\n      quantity,\n      now: new Date().toISOString(),\n    });\n\n    const subjectId = this.adminGrantSubjectId(\n      input.targetType,","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/entitlement/service.ts#L195-L231","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nif (env.selfhosted) {\n  throw new BadRequest('Self-hosted commercial entitlements require a signed license.');\n}\n\n// after — the caller side: branch before calling\nif (env.selfhosted) {\n  return uploadSelfHostLicense({ workspaceId, license });\n}\nreturn service.upsertAdminGrant(input);","handlingStrategy":"validation","validationCode":"function assertCanAdminGrant(env) {\n  if (env.selfhosted) {\n    throw new UserError('Use the license-upload flow on self-hosted, not admin grants');\n  }\n}","typeGuard":"function isSelfHostLicenseRequired(e: unknown): boolean {\n  return e instanceof Error && e.message === 'Self-hosted commercial entitlements require a signed license.';\n}","tryCatchPattern":"if (!env.selfhosted) {\n  await entitlement.upsertAdminGrant(input);\n} else {\n  await entitlement.upsertSelfHostLicense({ workspaceId, license });\n}","preventionTips":["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."],"tags":["entitlement","self-hosted","license","authorization","admin"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}