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

  1. On self-hosted, activate a signed license via the license activation API instead of admin grants
  2. If this environment is genuinely cloud, check why env.selfhosted is set (AFFI_NE env/selfhosted config) and correct it
  3. Use only free/built-in features on unlicensed self-hosted installs
  4. 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

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


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)