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

  1. On self-hosted, use the license-upload flow (resolveEntitlementV1 with a signed license) instead of admin grants.
  2. If you actually intend cloud behavior, verify env.selfhosted is false on this node and that you're hitting the right deployment.
  3. Update admin tooling to detect selfhosted and skip/warn on the grant path rather than erroring.
  4. 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

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.