Release authorization
Release authorization is an additional server-side ceiling for explicitly registered features. Access requires all four terms: release authorization, platform setting, organization entitlement, and the caller's normal permission. A tenant entitlement override cannot enable a release that is off.
packages/shared/src/release-features.ts maps each key to its platform setting,
entitlement, permission, and adopted surfaces. Unknown keys deny access. A new
registered key without a database row defaults to off. Verified existing
behavior can explicitly set grandfathered: true, which defaults to on; its
baseline description is not definition-of-done evidence.
States are off, preview, and on. Preview denies every identity in this
slice: previewAllows(ctx, featureKey) is the sole audience decision seam for
the subsequent signed preview-grants implementation. An audience JSON value,
cookie, flag, or client-supplied identity does not authorize preview access.
Server boundaries
- API routes opt in with
requireRelease(featureKey)after authentication and before handler effects. Denials return 404. Existing permission checks remain. - MCP tools opt in through the registry's
mcpToolsinventory. The production curated registrar prepares their authorization before connecting the SDK, omits denied tools from discovery, and rechecks direct invocations. Its returned synchronous names array remains the complete metadata inventory. Registration-only documentation recorders receive every tool without authenticating or connecting. A previously connected tool observes revocation on invocation; discovery is prepared per connection, so reconnect to discover newly enabled tools. - Job and email producers enclose the enqueue/send callback in
enqueueReleaseWork. Workers enclose the actual effect inexecuteReleaseWork, which checks current policy even for previously approved queued work. Persist identity and feature key with work, never a cached allow. - Server pages/actions enclose rendering or effects in
requireReleasePage. Its authorization callback must obtain a fresh authenticated server decision, forwarding the current principal without caching. Denials invoke Next's 404; authorization errors propagate before rendering.
These helpers require adoption at actual effect boundaries. This infrastructure
does not automatically guard all existing routes, pages, jobs, or emails. The
initial MCP adoption covers crm_reports_list and crm_report_run under
crm.reports, grandfathered as existing live behavior. Missing authorization
keeps CRM reports available; explicit off/preview still denies adopted MCP
surfaces and the server reports page. REST darkness is proved with a NEW test fixture, preserving shipped
CRM REST behavior. The reports page now calls requireReleasePage before rendering
its unchanged client component. Its server adapter forwards the current cookie
and authorization header to GET /v1/release-authorizations/:featureKey/decision?orgSlug=....
This session-only endpoint returns { allowed: boolean } using the authenticated
user, the selected organization's current policy, and all four access terms.
Both the HTTP response and server fetch disable caching; failed decisions stop
rendering. The admin policy endpoint is never used as an access decision.
The transition slice owns further surface adoption. Existing
getPlatformSetting, isFeatureEnabled, and requireFeature callers retain
their behavior.
Administration and audit
Verified platform administrators use authenticated
GET /v1/admin/release-authorizations/:featureKey and
PUT /v1/admin/release-authorizations/:featureKey. Unknown keys return 404.
GET returns { authorization }; a missing row reports version 0 and its
effective default state. PUT accepts state, audience (object, default {}),
version, and a nonblank reason. Send the last observed version; success
returns the incremented version in { authorization }. Stale writes return 409.
The transaction serializes even concurrent first writes for the same feature.
Migration 13599_release_authorizations.sql adds the state enum, authorization
table, and events table. A database trigger appends an event for each insert or
update in the same transaction, including direct SQL writers. Audit failure
rolls the policy write back. Updates and deletes of audit events are refused.
Database owners can still alter triggers or truncate tables; this is application
audit protection, not a boundary against database administrators.
The migration is additive. Disable a feature by writing off; rollback does
not require dropping tables or deleting audit history. Turning a feature off
prevents subsequent guarded execution but cannot undo an effect already sent.