--- id: netkingdom-tenant-engine-boundary-contract type: standard title: "NetKingdom Tenant Engine Boundary Contract v0.1" domain: netkingdom status: accepted version: "0.1" created: "2026-07-23" updated: "2026-08-22" scope: tenant-domain-boundaries adr: - docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md - docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md - docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md related: - canon/standards/iam-profile_v0.3.md - canon/standards/user-engine-boundary-contract_v0.1.md - canon/standards/credential-management_v0.2.md - docs/platform-identity-security-architecture.md --- # NetKingdom Tenant Engine Boundary Contract v0.1 ## Purpose This contract defines how `tenant-engine` integrates into the NetKingdom landscape without duplicating identity, authorization, user/membership, pricing, deployment, or secret-custody responsibilities that other services already own. `tenant-engine` owns **tenant-as-an-entity** facts: existence, identifier and grouping (ADR-0013), capability roles (ADR-0014), plan/subscription assignment, and — reserved, not yet implemented — guardrail/quota policy. It is not an identity provider, not a policy decision point, not a billing processor, not a pricing-model engine, and not the user/membership domain. NetKingdom owns this cross-repo boundary contract; `tenant-engine` and its adapters must preserve the ownership rules below even in local/standalone deployments. ## Ownership Model | Concern | Owner | tenant-engine responsibility | | --- | --- | --- | | OIDC discovery, login, token issuance | `key-cape`, Keycloak | Supply `tenant_roles` via a read API at issuance time; never issues tokens itself | | Tenant identifier format and grouping vocabulary | NetKingdom (`iam-profile_v0.3.md`, ADR-0013) | Store and validate tenant records against the ratified vocabulary; does not redefine it | | Tenant capability-role vocabulary | NetKingdom (ADR-0014) | Store role grants against the ratified vocabulary (`PLTF`/`IAM`/`VEN`/`CUS`); does not redefine it | | Users, profiles, memberships | `user-engine` | References `tenant_id` as an external key only; never stores user or membership records | | Fine-grained authorization decisions | `flex-auth` | Serves as an authoritative data source `flex-auth` queries live for high-stakes checks; makes no authorization decisions itself | | Pricing-model / plan definitions | `adaptive-pricing` | References plan definitions by id; never forks or duplicates plan terms locally | | Runtime secrets | OpenBao / `secrets-engine` | Tenant-scoped OpenBao paths (e.g. `tenants//...`) reference `tenant-engine`'s tenant registry as the naming authority; `tenant-engine` never stores secret material | | Deployment mechanics | Railiance | Publishes readiness signals only | | Credential routing/discovery | `ops-warden` | `tenant-engine`'s own service credentials (if any) are routed the same way as any other lane; `tenant-engine` is not a routing authority | ## Source-Of-Truth Matrix | Resource kind | Source of truth | tenant-engine relation | Boundary rule | | --- | --- | --- | --- | | Tenant record (existence, current grouping, name/slug) | `tenant-engine` | Canonical owner | Current grouping must be one of ADR-0013's taxonomy, or the reserved ungrouped `platform`/`coulomb` classifications; the identifier segment is historical and must not be used as current grouping | | Tenant capability roles | `tenant-engine` | Canonical owner | Non-exclusive; grant/revoke are audited mutations (see Grant Contract below), never free-form field updates | | Plan/subscription assignment | `tenant-engine` | Canonical owner | References an `adaptive-pricing` plan id; does not define plan terms | | Pricing-model / plan definitions | `adaptive-pricing` | Consume by reference only | `tenant-engine` must not cache plan terms beyond what's needed to resolve which roles a plan currently grants | | Guardrail/quota policy (spend limits, entity/action counts) | `tenant-engine` (reserved) | Canonical owner once designed | Not implemented by this contract version — reserved namespace only, see Guardrail Policy section | | `tenant_roles` token claim | `tenant-engine` (live) / `key-cape` (cached copy) | `tenant-engine` is authoritative; the token claim is a point-in-time cache | Privileged/high-stakes decisions MUST re-query `tenant-engine` live — see IAM Profile v0.3, "Tenant Roles" section — never trust the cached claim alone | | Tenant identifier claim shape (`tenant`) | NetKingdom / IAM Profile contract | `tenant-engine` validates against it; does not mint the claim format | The wire format stays owned by the profile; the identifier is immutable, including its historical grouping segment; `tenant-engine` owns which concrete tenant values currently exist | | User/membership records scoped by a tenant | `user-engine` | No relation | `tenant_id` is the only key shared between the two services; `tenant-engine` never stores or reads user data | | Authorization decisions | `flex-auth` | Data source only | `tenant-engine` never enforces access itself; it answers queries `flex-auth`'s policy packages issue | The tenant identifier and current grouping are intentionally independent after creation. `tenant-engine` MAY change the record's `grouping` through an audited mutation without renaming `tenant_id`. Reads and domain events expose current grouping explicitly. No API, guardrail, pricing, or authorization consumer may recover current grouping by parsing `tenant_id`. ## Tenant Role & Plan Grant Contract Every role grant or revocation is an audited mutation, not a direct field write, and must carry: | Field | Requirement | | --- | --- | | `tenant_id` | Stable tenant identifier | | `role` | One of `PLTF`, `IAM`, `VEN`, `CUS` | | `grant_reason` | `plan_assignment`, `manual_grant`, or `platform_default` | | `plan_id` | `adaptive-pricing` plan id; required when `grant_reason: plan_assignment`, otherwise null | | `granted_by` | Subject or system that performed the grant | | `granted_at` | Timestamp | | `revoked_at` | Nullable timestamp | | `correlation_id` | Request/import correlation id | Rules: - Plan-linked roles (starting with `IAM`) are granted and revoked automatically as a consequence of plan-assignment changes in `tenant-engine`; manual grants are the exception path and must always record `granted_by`. - `trial`-grouped tenants may hold any role without a plan requirement — `grant_reason: platform_default` is valid for trial-eligible roles with no `plan_id` (ADR-0014). - Every grant or revocation emits a domain event. After a revocation, `key-cape`'s previously-cached `tenant_roles` claim is considered stale until the token naturally expires — v0.1 relies on the profile's short token lifetimes for bounded staleness, not push invalidation. - Cross-tenant grants are rejected; a grant always targets exactly one `tenant_id`. ## Authorization Contract `tenant-engine` is a data source, not a policy enforcement point. `flex-auth` remains the policy decision point for authorization outcomes; `key-cape` remains the token issuer. `tenant-engine` exposes: - a **read API** `key-cape` calls at token-issuance time to source the cached `tenant_roles` claim; - a **live-lookup API** `flex-auth` calls for high-stakes decisions (`assurance.level >= aal2`-class actions, per IAM Profile v0.3), returning current roles, grouping, and plan state; - a **write API**, authorization-gated by `flex-auth` itself, for grant/ revoke/plan-assignment mutations — `tenant-engine` does not self-authorize its own writes. Performance model: - the live-lookup API must be fast and highly available, since `flex-auth` is required to call it synchronously on the request path for privileged actions; slow or unavailable `tenant-engine` lookups fail closed (deny), never fail open. The adapter existing without a production caller does not satisfy this contract and does not permit either service to claim I3; - the cached-claim read API used by `key-cape` may be slower, since it only runs at token issuance, not per-request; - request-scoped memoization within a single `flex-auth` decision is allowed; cross-request caching of live-lookup results is not, since it would recreate the staleness risk the live path exists to avoid. ## Guardrail Policy (Reserved, Not Implemented) Named here so the shape exists before it's designed, matching ADR-0014: - Spend limits — `trial`-grouped tenants default to a **zero** spend budget. - Entity count limits — reserved, not yet specified. - Action count limits — reserved, not yet specified. - Enforcement point, override process, and how guardrails interact with capability roles (a `trial` tenant holding `VEN` is still bound by `trial` guardrails) are all open design questions for `tenant-engine`'s own workplan, not resolved by this contract. ## Audit Correlation Contract Every `tenant-engine` mutation (tenant creation, role grant/revoke, plan assignment) must create or reference a correlation bundle: | Field | Requirement | | --- | --- | | `correlation_id` | Stable id propagated across request, decision, audit, and event records | | `actor` | IAM Profile actor envelope (who performed the mutation) | | `tenant_id` | Tenant affected | | `resource` and `action` | e.g. `tenant-engine:role`, `grant` / `revoke` | | `authorization_decision_id` | `flex-auth` decision id backing the write | | `outbox_event_id` | Event id emitted for the mutation | | `change_summary` | Redacted mutation summary | ## Non-Goals - `tenant-engine` does not process payments or hold financial instruments — it records which plan a tenant is assigned to, sourced from whatever system actually processes payment (not yet identified; out of scope for this contract). - `tenant-engine` does not define pricing models, tiers, or terms — `adaptive-pricing` does. - `tenant-engine` does not store user accounts, profiles, or memberships — `user-engine` does. - `tenant-engine` does not make authorization decisions — `flex-auth` does.