--- id: NK-ADR-0014 type: architecture-decision-record title: "Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership" status: accepted owner: net-kingdom revision: "1" decided: "2026-07-23" last_reviewed: "2026-08-22" review_interval: 12m --- # ADR-0014 - Tenant Capability Roles, Carrying Mechanism, and Tenant-Engine Ownership **Status:** Accepted **Date:** 2026-07-23 **Deciders:** Bernd Worsch, Codex ## Context ADR-0013 introduced the tenant onboarding grouping taxonomy (`trial`/`friendly`/`single`/.../`agentic`), deliberately orthogonal to a separate, unratified **capability-role** model sketched in `docs/princedom-isolation-exploration.md`: `PLTF` (operates the platform), `IAM` (organizes its own users/auth/secrets), `VEN` (provides apps/services to others), `CUS` (consumes apps/services from `PLTF` or `VEN` tenants) — non-exclusive, a tenant may hold several at once. That exploration left open where capability roles actually live (a per-token claim vs. a registry), who owns them, how they're granted or revoked, and how this interacts with the IAM Profile's existing `roles` claim — which is a *per-subject* claim ("coarse identity roles" for the human/service/agent holding the token), a different concept from a *per-tenant* capability fact. Conflating the two would be a category error: `roles: ["VEN"]` on a token would ambiguously mean "this subject has vendor-role" vs. "this subject's tenant is a vendor." No existing service owns tenant-as-an-entity facts (existence, grouping, capability roles, plan/subscription state) as a queryable resource. `user-engine`'s own boundary contract (`canon/standards/user-engine-boundary-contract_v0.1.md`) explicitly scopes `user-engine` to *consuming* tenant identifiers and *storing tenant-scoped records*, not owning tenant identity or capability facts. Bernd's direction (2026-07-23): - Implement the previously-discussed hybrid carrying approach: cache a tenant's capability roles on the token for ordinary decisions, but require a live check for critical/high-stakes actions. - Role grants are usually tied to a payment plan — most concretely, `IAM` means the tenant has its own dedicated key-cape/Keycloak instance for isolation, scale, and performance, which is itself a paid capability. - `trial`-grouped tenants may hold **any** capability role without restriction, specifically so the platform can showcase, test, and explore every role. Safety for trial tenants comes from resource guardrails (spend limits defaulting to zero budget, entity/action count limits), not from role gating — guardrail design itself is future work, not this ADR. - A new service, `tenant-engine`, will be built (Bernd) as the owner of this domain, organized beside `user-engine` rather than inside it — smaller, single-purpose services are easier to reason about and drift less, matching the fleet's existing convention (`activity-core`, `audit-core`, `user-engine`, and others). ## Decision 1. **Capability-role vocabulary ratified as core NetKingdom vocabulary**: `PLTF`, `IAM`, `VEN`, `CUS`, non-exclusive. `IAM` specifically means: the tenant operates its own dedicated IAM implementation instance (lightweight key-cape or expanded Keycloak) rather than sharing the platform's, for isolation/scale/performance — not "any tenant that happens to have users." 2. **`tenant-engine` is the canonical owner** of tenant-domain facts: tenant existence, grouping (ADR-0013), capability roles (this ADR), plan/ subscription assignment, and — reserved for future design, not built now — guardrail/quota policy. It is a new, separate service, not a module inside `user-engine`. Its ownership boundary is defined in the companion contract, `canon/standards/tenant-engine-boundary-contract_v0.1.md`. 3. **Carrying mechanism: hybrid cache + live re-validation.** `tenant-engine` is the single source of truth. `key-cape` stamps a cached, optional `tenant_roles` claim onto issued tokens at issuance time, sourced from `tenant-engine` (added to the IAM Profile as a new optional claim — `canon/standards/iam-profile_v0.3.md`). Consumers may trust the cached claim for ordinary decisions. `flex-auth` **MUST** re-validate live against `tenant-engine` — never trust the cached claim alone — before authorizing privileged or high-stakes actions, using the same threshold class the profile already defines for `assurance.level >= aal2` (privileged, destructive, platform-root, secret, credential-vending flows). This bounds staleness risk for ordinary actions to a token's short lifetime (5–30 minutes for service/agent tokens, per the profile's Token Lifecycle table) while guaranteeing freshness exactly where it matters most. 4. **Role governance is plan-linked.** Granting a role is normally a consequence of a tenant's payment-plan state in `tenant-engine`, not a separate manual workflow — starting with `IAM`. `tenant-engine` records which plan grants which role(s); `adaptive-pricing` remains the source of plan/pricing-model *definitions*, `tenant-engine` owns the tenant's *current* plan/subscription assignment, referenced by id, never duplicated locally. Whether `VEN` needs an approval gate beyond payment (reselling access carries legal/compliance exposure a payment alone doesn't cover) is **not resolved by this ADR** — left to `tenant-engine`'s own workplan. 5. **Trial tenants may hold any capability role, unrestricted.** The `trial` grouping's purpose (showcase, test, explore) requires demonstrating every role. Safety is enforced through resource guardrails instead: trial tenants default to a **spend budget of zero**, with entity and action count limits to follow. Guardrail policy design (exact limits, enforcement point, override process) is real, near-term future work, reserved as `tenant-engine`'s to own once designed — not specified by this ADR. 6. **Grouping and capability role are independent axes** recorded on the same tenant record in `tenant-engine`. Neither constrains the other except where a future guardrail policy explicitly says so. ## Consequences - `canon/standards/iam-profile_v0.3.md` adds the optional `tenant_roles` claim and folds in ADR-0013's tenant-identifier vocabulary update (both non-breaking per ADR-0011's own minor-version rule — no existing implementation is invalidated by either change). Supersedes `iam-profile_v0.2.md`. - `tenant-engine` becomes a new repository with its own workplans (Bernd). `canon/standards/tenant-engine-boundary-contract_v0.1.md` defines its ownership boundary now, before code exists — the same sequencing `user-engine`'s contract followed. - `flex-auth` policy packages gating high-stakes actions must add a `tenant-engine` live-lookup step; they cannot trust `tenant_roles` alone for those decisions. - `key-cape` needs a `tenant-engine` integration at token-issuance time to source the cached claim — tracked in `key-cape`'s own workplans, not here. - `docs/platform-identity-security-architecture.md`'s Tenant Model section is updated to reflect the grouping + role split and `tenant-engine`'s role (companion change alongside this ADR). - Guardrail/quota policy is named as required near-term work and given an owner (`tenant-engine`), but is explicitly not designed by this ADR. ## Alternatives Considered ### Token-claim-only, no live re-validation Rejected: staleness would be unbounded within a token's lifetime for genuinely high-stakes actions. A stale `VEN` grant surviving a plan downgrade or cancellation is not an acceptable risk for money-movement or credential-vending flows — exactly the class the profile already treats as requiring the strongest assurance. ### Registry-only, no cached claim Rejected: every ordinary request would pay a `tenant-engine` round-trip even for non-critical checks, adding latency and a hard runtime dependency for every consumer, not just the ones handling privileged actions. ### Restrict role eligibility by tenant grouping (e.g., `trial` cannot hold `VEN`) Rejected per Bernd's direction: `trial` tenants exist specifically to showcase every role. Resource guardrails are the intended safety mechanism instead, keeping the two axes (grouping, role) independent. ### Put tenant-role/plan storage inside `user-engine` Rejected. `user-engine`'s own boundary contract scopes it to consuming tenant identifiers and storing tenant-*scoped* user records, not owning tenant-as-an-entity facts. A dedicated service avoids coupling a security-critical, high-frequency lookup (used by `flex-auth` on every privileged decision, and by `key-cape` on every token issuance) to `user-engine`'s much larger surface (registration flows, factor models, family dataspace onboarding) that has nothing to do with tenant capability state. ## Follow-Up - `tenant-engine` repository creation and its own workplan (Bernd). - `key-cape` integration: source `tenant_roles` from `tenant-engine` at token issuance. - `flex-auth` policy package updates: live `tenant-engine` re-validation gate for privileged actions. - Guardrail/quota policy design for `trial` (and eventually all) tenants: spend limits, entity/action count limits, enforcement point, override process. - Resolve whether `VEN` needs an approval gate beyond payment-plan state.