ADR-0011 Public Tenant Numbers and Hybrid Resource Routes
Status
Section titled “Status”Accepted on 2026-08-10.
Context
Section titled “Context”Tenant UUIDs were exposed in every tenant-owned API path. This made browser and operator-facing tenant selection needlessly opaque and repeated the Tenant identifier even after a globally unique aggregate ID was known. Deriving Tenant scope from a header or UI session would shorten URLs, but would also make the authorization boundary implicit and create ambient request state.
REST does not prescribe one URL hierarchy. Hierarchical resource names are useful for collection selection and creation, while globally unique aggregate IDs allow a shallower canonical member URL.
Decision
Section titled “Decision”TenantId remains the internal UUID used by tables, foreign keys, repository
methods, events, audits, and authorization. TenantNumber is a positive,
immutable, globally unique int32 generated by PostgreSQL and exposed for
Tenant selection.
The API uses this hybrid rule:
- Tenant collections, creates, projections, and singletons use
/v1/tenants/{tenantNumber}/…. - A globally unique aggregate member uses a flat canonical route, for example
/v1/devices/{deviceId},/v1/memberships/{membershipId}, or/v1/flows/{flowId}/versions. - Dependent resources stay below their aggregate root. Globally unique child IDs do not automatically receive additional top-level endpoints.
- The Tenant is never inferred from a request header or UI session.
- Removed tenant-nested member routes have no aliases and return
404.
A typed resource locator resolves the internal Tenant UUID before authorization
on flat routes. It has one fixed method per aggregate and accepts no dynamic
table names. Services and repositories still require both the resolved Tenant
UUID and the resource ID, so mutations remain tenant-scoped inside their
transaction. Raw-ingestion replay resolves the composite key consisting of the
Raw ID and receivedAt.
The Web Console deliberately keeps a visible numeric workspace path such as
/tenants/17/devices/{deviceId}. It uses the number for navigation and API
collections, but the internal UUID for local permission evaluation.
Consequences
Section titled “Consequences”Tenant numbers are discoverable labels, not authorization evidence. A user with access to several Tenants can submit a flat mutation for a resource in the wrong—but still authorized—Tenant if they confuse aggregate IDs. We accept this trade-off rather than requiring a redundant Tenant header. The UI preserves a visible Tenant workspace to reduce this risk.
Unknown or inaccessible flat resources both return 404. A known Tenant for
which the actor lacks the required role keeps the existing 403 behavior.
Invalid numeric Tenant syntax returns 400 after authentication. Tenant numbers
must not become metrics labels.
The API change is breaking: clients must coordinate their rollout and there is
no compatibility alias. After numeric routes are released, production recovery
is a forward fix; rolling the database column down would invalidate the public
contract. The expected platform release impact is major; this decision does
not itself authorize or perform a version bump.
Alternatives rejected
Section titled “Alternatives rejected”- Keep UUIDs public everywhere: secure but unnecessarily opaque and repetitive.
- Use only flat collections: loses explicit Tenant selection for list and create operations.
- Use
X-Tenant-IDor session state: hides security-relevant scope in ambient state and makes links less self-describing. - Replace internal UUID foreign keys with numbers: creates a large, risky data migration without improving isolation or repository contracts.
This decision is consistent with Fielding’s REST architectural style, Google AIP-122, and the Microsoft Azure REST API Guidelines.