AUTH4D-39 plan entitlements
Plan entitlements gate developer management actions only. End-player authentication (sign-in, token, refresh, device and guest flows) never reads billing state, so an unpaid or over-limit project keeps serving its players.
Effective entitlements (packages/tenancy getEntitlements / createEntitlementService)
The service reads the tenant’s newest active or past_due subscription through
@auth4/billing, and catalog limits come from PLAN_CATALOG. Every query filters by the tenant
id.
| Subscription state | Effective plan | source |
|---|---|---|
none (or only pending / canceled / expired) |
free |
free |
active, before current_period_end |
subscribed plan | subscription |
active with cancel_at_period_end, after period end |
free |
lapsed |
past_due (or unrenewed active) before period end + 7d |
subscribed plan | grace_period |
past_due (or unrenewed active) after period end + 7d |
free |
lapsed |
The grace period is BILLING_GRACE_PERIOD_DAYS (7). When a past_due subscription has no period
end, updated_at is the start of the grace period. The service is read-only. Moving a
subscription to expired remains a billing state-machine transition (expireSubscription) run by
the billing jobs.
Application limit
POST /management/tenants/{tenantId}/clients and re-enabling a disabled application
(PUT/PATCH .../clients/{clientId} with enabled: true) need a free slot under
entitlements.maxApplications. Only enabled applications count, so disabling (DELETE) an
application frees a slot. A request over the limit returns:
HTTP/1.1 402 Payment Required
Cache-Control: no-store, max-age=0
{
"error": {
"code": "plan_limit_exceeded",
"message": "The current plan limit has been reached. Upgrade the plan to continue.",
"request_id": "req_...",
"details": [{ "field": "maxApplications", "issue": "The free plan allows 2 enabled applications." }]
}
}
The body conforms to ManagementErrorResponseSchema. Authorization runs first, so callers without
access still receive 403 forbidden or 403 insufficient_role. The existing hard cap of 50 rows per
project (409 client_limit_reached) is unchanged. The limit is also checked again inside the
write batch, so concurrent creates cannot exceed it. A denied request writes no client,
idempotency or audit row. Applications above the limit after a downgrade stay enabled and keep
working, but no further applications can be enabled.
Usage reporting
GET /management/tenants/{tenantId}/usage (scope tenant:read; project roles owner, admin,
viewer):
{
"usage": {
"tenantId": "ten_...",
"planId": "free",
"source": "free",
"graceEndsAt": null,
"period": { "start": "2026-10-01T00:00:00.000Z", "end": "2026-11-01T00:00:00.000Z" },
"monthlyActiveUsers": { "count": 2, "limit": 1000, "overLimit": false },
"applications": { "count": 2, "limit": 2, "overLimit": false }
}
}
MAU is the number of distinct end users in the tenant whose session was created or refreshed in
the current UTC calendar month (session_indexes.last_seen_at). Sign-in and refresh already write
those rows, so MAU counting adds no work or failure mode to authentication. MAU over the limit is
reported only and never enforced. Hosts can pass onUsageReport to createEntitlementService to
forward reports to metrics or notifications. The callback is called before the report returns but
is not awaited, and its errors are ignored. The API uses it for the AUTH4D-309 owner alerts at 80%
and 100% of the limit (see auth4d-309.md).
Source: docs/api-spec/auth4d-39.md