## Status

Proposed, draft-13; ratification-ready. Relocated from `the-custodian/canon/architecture` on 2026-08-17: multi-tenancy is part of the IT-security framework NetKingdom provides, so this framework belongs in NetKingdom canon beside the IAM Profile and the tenant-engine boundary contract, not in the work-factory canon.

- draft-1 proposed a single model with fixed characteristics. Rejected: it could not describe a repo that is not there yet.
- draft-2 reframed to graduated levels per axis. Externally corroborated (§16), but four of its statements were wrong and one thing it needed was missing.
- draft-3 applied those corrections, added the retention axis, and recorded an adoption stance.
- draft-4 closed the two gaps draft-3 left open: `R4` had no mechanism beyond waiting, and the noisy-neighbour evidence artifact asserted something shared infrastructure cannot provide.
- draft-5 relocated to NetKingdom and renamed the dimensions from planes to axes, because the word was already taken (§0).
- draft-6 applied `tenant-engine`'s review: five changes, including an axis that did not fit its data shape.
- draft-7 applies `audit-core`, `railiance-platform` and `flex-auth`. Eleven further changes, two of them corrections to statements this document made as fact about other repos. Every posture I guessed was too generous, on every repo that has now self-reported.
- draft-8 applies `adaptive-pricing`'s review, the last of the six, and the consistency review across all declarations. It adds the missing availability axis, a canonical declaration schema, explicit authority for tier assurance, retention/placement coupling, downgrade propagation, and honest sanctioned customer language. It also corrects the distinction between an implemented control and an evidenced current level.

- draft-9 answers `zone-engine`'s `ZONE-WP-0001-T01`. It rules that enforcement stance is not a seventh axis (Decision 5.6) while reserving `zones:` in `tenancy.yaml` so the estate keeps one declaration surface, and it records the reef/`P`/`V` reconciliation as an open defect of this document rather than of the repo that noticed it (Decision 8.4).
- draft-10 answers `zone-engine`'s `ZONE-WP-0001-T03`. It keeps the workload as the sole security-zone policy subject, makes operational and control-plane execution units part of that term, and requires unresolved workload identity or membership to remain `unknown` without zone inference (Decision 5.6.1). It also closes the textual reef/`P` boundary while leaving the substrate-provider declaration and mechanical `V` join as implementation work (Decision 8.4.2).
- draft-11 makes that ruling declarable. A `zones:` block now requires an authoritative `workload_identity` beside it, including for non-`rapp` operational workloads; multi-service files carry both per service. Missing identity remains absence rather than a guessed join (Decision 5.6.2).
- draft-12 reconciles that declaration with RMGR-ADR-004 and the published `security-zones_v0.1` proposal. Managed deployables use their authoritative rapp declaration and the exact Repo Manager reference tuple; independently governed operational execution units that are not managed deployables may declare locally. Native actions, actors, lanes, patterns, and resources are explicitly `not-applicable`, while omitted or unresolved workload references remain `unknown` (Decision 5.6.2).
- draft-13 advances the `audit-core` worked example from E1 to E2 after bounded adversarial run `WH-ENG-20260822-AUDIT-E2-03` supplied the artifact required by §13.2. The claim remains explicitly bounded and freshness-dated: the attempted cross-tenant attacks did not work; this is not a universal isolation proof.
- draft-14 makes evidence freshness and remediation ownership declarable. Adversarial evidence may now carry its observation and expiry timestamps, bounded scope, responsible repository, and replacement action. A separate proposal-only evaluator treats absent authoritative owner or freshness as `unknown`; it does not infer either or mutate the declared posture.

Reviewed by all six. The score: six repos found three live defects in their own code by reading the ladders — `tenant-engine`'s unfiltered event accessor, `audit-core`'s unfiltered read path, `flex-auth`'s unauthenticated `/v1/check` — and `railiance-platform` found `apps-pg` running with no backup configured at all while writing its §10.2 disclosure. The framework changed to fit the repos; no repo was told to fabricate a posture.

Informed by five external research digests plus their index in `the-custodian/research/2026-08-17-adr008-*`, which carry full citations for the external claims made here.

## 00Terminology: axes, not planes

`docs/platform-identity-security-architecture.md` — accepted, 2026-07-23 — already uses plane for a trust and deployment layer: the bootstrap plane, the platform control plane, and tenant planes. That meaning is established, ratified, and owned by this repo.

Drafts 1–4 of this document, written elsewhere, used plane for something different: an independent dimension of concern. Two incompatible senses of one word inside one canon is exactly the concept-ownership collision the estate has been careful about elsewhere, and the newcomer yields.

This framework therefore describes six axes. They are orthogonal to NetKingdom's planes, not a subdivision of them:

- A plane is where something runs and what trust it carries — bootstrap, platform control, tenant.
- An axis is which property of tenancy is being described — identity, authorization, enforcement, placement, retention, availability.

A workload in the tenant plane has a position on all six axes. A platform control plane service does too. The two vocabularies compose and neither replaces the other.

The rename is also an improvement. A posture vector is literally a point in six-dimensional space, and "axis" says that where "plane" did not.

## 01Context

Drafts 1–4 opened by claiming the estate "has never written down what it is building". Relocation proved that wrong, and the correction is worth keeping visible: `docs/platform-identity-security-architecture.md` has described the trust model, the tenant model and a capability progression since 2026-07-23. The accurate claim is narrower — what was missing is a way to say how far a given service has got, and to hold several answers at once. Seven documents cover slices of the subject and none of them does that:

Document | Covers | Status
--- | --- | ---
`iam-profile_v0.3` (NetKingdom) | Tenant identifier shape, `tenant_roles` claim, staleness rules | Ratified
`tenant-engine-boundary-contract_v0.1` (NetKingdom) | Who owns tenant records, roles, plan assignment | Ratified
`business-app-service-contract_v0.1` §1 (Custodian) | Business apps: instance-per-client, tenant-keyed data | Ratified
`rapp-postgres` ADR-0001 | Consumer + tenant isolation in PostgreSQL | Proposed, governs one repo
`rapp-postgres` ADR-0002 | Per-consumer retention and the erasure horizon | Proposed, governs one repo
`shared-platform-relational-storage_v0.1` | The stacked-boundary gap | Routed 2026-08-10, still unratified
`platform-identity-security-architecture` (NetKingdom) | Trust model, planes, tenant model, capability progression | Accepted 2026-07-23

This document is downstream of that architecture and must not restate it. It answers one question the architecture leaves open: given the model, where is this particular service today, and how would anyone know?

Four failures existed when drafting began.

The gap was diagnosed once and the fix stalled. The v0.1 draft was written to fill this hole and has sat unratified in neither canon directory. §20 attaches a ratification path so this one does not join it.

Placement was owned by nobody. `user-engine-pg` and `target-revenue-pg` are dedicated; `apps-pg`, `net-kingdom-pg`, `platform-pg`, `state-hub-db` and `forgejo-db` are shared. Both live, neither written down. `tenant-engine` raised this with `railiance-platform` on 2026-08-16. Draft-8 resolves the authority split in §8.2.

Two contradictory defaults were already ratified. Business apps get instance-per-client; platform services pool. Nothing says which shape a new service takes, and no definition separates the categories. Decision 4.4.1 now supplies the default; §19.4 retains the missing classification rule.

There is no honest way to describe a repo that is not there yet. The estate absorbs repos with weak or absent tenant separation. Today such a repo is simply non-conformant, leaving it two bad options: misrepresent its posture, or stay outside the framework.

## 02What this document is

A framework, not a model. It specifies no single correct implementation. It supplies terminology (§3, §4), a declaration (§5), a conformance rule (§6), methodology (§12), and evidence definitions (§13).

A service is conformant when its declared posture is accurate and its trajectory recorded. A service is non-conformant when it claims a level it cannot evidence — regardless of how high or low that level is.

## 03Six orthogonal axes

"Is this multi-tenant?" is treated as one question. It is six, and they are independent:

Axis | Question | Vocabulary owner
--- | --- | ---
Identity (I) | How is a tenant named and validated? | `tenant-engine` / IAM Profile
Authorization (A) | How is a request bound to the tenants it may act for? | `flex-auth`
Enforcement (E) | Where, mechanically, is the tenant boundary enforced? | This framework
Placement (P) | Which substrate holds a tenant's data? | `railiance-platform`
Retention (R) | How long does data persist, and how is it erased? | The storage platform; policy by the consumer
Availability (V) | What failure can the complete service path survive, and within what recovery objective? | The delivering service; substrate facts by its providers

Conflation produces errors today. `rapp-postgres`'s `PostgresConsumer` carries `tenantIsolation: consumer-service-boundary` — an E-axis fact in a P-axis artifact, reading as though storage enforces something it does not. The "dedicated versus shared" argument mixes P (capacity, blast radius) with E (correctness).

The axes are separated precisely so each may sit at a different level.

Decision 3.1: every document, declaration and plan tier that says "isolation" MUST name which axis it means.

Decision 3.2: the axes couple at their tops and the couplings MUST be stated where they apply, not used to argue the axes are one:

- `E4` is reachable only at `P3` or above.
- `R`'s erasure horizon is bounded below by `P` — on shared substrate, a consumer's horizon is the instance maximum (§4.5).
- `R4` by key destruction is bounded by the key boundary, which is an E-axis property. Shredding a single tenant's data requires the application to encrypt under a per-tenant key before writing; the storage platform cannot supply it. Reaching the top of the retention ladder is not a retention project.
- `V` composes as the minimum across the critical request path, not the maximum of its components. A replicated application on a single-instance database is not V2. A tested degraded mode may remove a dependency from that path, but the bypass itself is part of the V evidence.

Decision 3.3 — scope. The P and R ladders describe a service's primary datastore. The V ladder describes the service's complete critical request path, including providers it synchronously depends on. Caches, search indices, message queues and background jobs are named leak surfaces in the external baselines and are assessed separately, not silently covered by a datastore level. A declaration names material secondary stores and asynchronous paths as exceptions rather than implying that one vector proves them safe.

## 04Graduated levels

Each axis carries an ordered ladder. Higher is stronger, not better: the right level is the one a service can evidence and its risk warrants.

### 4.1 Identity (I)

Identityaxis I
I0No tenant concept. Data not attributable to a tenant.
I1A local tenant notion exists but is not canonical, or the tenant is taken from the request rather than from a verified token.
I2Canonical identifiers, bound at the identity provider and carried as a verified claim, and verified by this service on its own inbound calls.
I3I2 plus capability roles honoured, with live `tenant-engine` re-query for privileged, destructive, credential-vending or `aal2`-class decisions.
—Ladder ends at I3.

I1 now explicitly absorbs request-supplied tenant identifiers. "Never trust client-supplied tenant IDs without validation" is a named anti-pattern; a service reading the tenant from a header is at I1 however canonical the string.

An axis is assessed on a service's own inbound surface, never on its authority over the concept. `tenant-engine` is the source of existence for tenant records and is nonetheless at I1, because it takes the acting identity from the request body rather than from a verified token. Draft-5 conflated these by naming the authority inside the I2 definition, which made the level describing canonical identity unclaimable by the service that provides it. Corrected on tenant-engine's review — a reader would otherwise assume the authority must be at I2 by definition.

`business-app-service-contract` §2.1 sets app-local accounts as the v1 baseline for business apps — a sanctioned low level with recorded triggers for moving up. That is the pattern this framework generalises.

### 4.2 Authorization (A)

Authorizationaxis A
A0No authorization, or tenant context not carried.
A1Ad-hoc checks scattered through handlers.
A2A single local authorization boundary; tenant context bound once, centrally.
A3Decisions delegated to `flex-auth` as PDP, with live re-query where the IAM Profile requires it.
A4A3 over a standard PDP interface (OpenID AuthZEN Authorization API 1.0), so the decision point is swappable and the enforcement point is not coupled to one engine's request shape.

This ladder describes enforcement points. A decision point cannot occupy A3 — "delegated to `flex-auth`" is not something `flex-auth` can do. A service that is a PDP declares two numbers: its own inbound level, and the maximum it enables for consumers. `flex-auth` reads `A0, enables A3` — accurate, and considerably more alarming than `A3`, which is the point. Raised by `flex-auth`, whose absence from the §5 worked examples was this surfacing implicitly.

A4 is new. The specification reached Final in January 2026 and Keycloak shipped experimental support in May; the argument for it is interoperability — a swappable decision point and an enforcement point not coupled to one engine's request shape.

Correction from flex-auth's review: earlier drafts also justified A4 as ending the copying of action strings between repos. It does not. AuthZEN standardises the envelope — subject, action, resource, context, endpoint — and deliberately does not standardise the action vocabulary or the policy language. At A4, `tenant.guardrail.set` still has to be agreed and still gets copied. Those are two problems with different fixes, and the cheaper one is not A4: `flex-auth`'s registry already carries action definitions per system and could serve them read-only. The vocabulary argument is withdrawn.

Internal service-to-service calls are in scope for this axis. "Skipping tenant validation for internal services" is a named anti-pattern and our estate is mostly internal calls.

Correction from flex-auth's review: earlier drafts asserted that `flex-auth` calls `tenant-engine` synchronously on the authorization path. That is not true. The adapter is built and complete and has no non-test caller, so the IAM Profile's live re-query exists and is unwired — which is also why `flex-auth` cannot reach I3. Built-and-unwired is the worst of the three states because it reads as capability.

The requirement, narrowed on their proposal because the original was too strong to be met and would have made `tenant-engine` a hard availability dependency of every decision in the estate:

Tenant context MUST be carried on every internal hop and MUST NOT be re-derived from a service identity. It MUST be revalidated against `tenant-engine` at least once per request chain — at the service that holds or mutates the tenant's data, or before a privileged, destructive, credential-vending or `aal2`-class decision, whichever comes first. A hop that neither holds tenant data nor makes such a decision may carry the context without revalidating it.

And carrying tenant context is worthless without an authenticated hop to carry it over. `flex-auth` found this in itself: it carries tenant context faithfully and cannot distinguish "user-engine asking on behalf of tenant X" from "any pod asking on behalf of tenant X".

### 4.3 Enforcement (E)

Enforcementaxis E
E0None. Data not tenant-keyed; separation incidental or absent.
E1Data tenant-keyed, filtering applied per query at call sites.
E2Filtering centralised at a single service-side choke point binding authenticated identity to permitted tenants.
E3E2 plus platform-assisted filtering: row-level security keyed on a tenant GUC set transaction-locally, or an equivalent enforced data-access layer.
E4Structural: the credential a workload holds cannot address another tenant's data at all. Requires per-tenant credentials and per-tenant substrate.

Correction from draft-2. Draft-2 described E3 as something "the application cannot trivially route around". That is false and it was this document overclaiming in exactly the way §6 prohibits. Any session can re-issue `SET` on a custom GUC, so an attacker with SQL execution can reset the tenant and read across the boundary. What E3 buys is precise, and the ladder must say so:

Threat | E1 | E2 | E3 | E4
--- | --- | --- | --- | ---
A developer forgets a tenant predicate | ✗ | ✓ | ✓ | ✓
A new code path bypasses the choke point | ✗ | ✗ | ✓ | ✓
SQL injection reaching the connection | ✗ | ✗ | ✗ | ✓
The application process is compromised | ✗ | ✗ | ✗ | ✓

E3 is a strong control against accident — the common case, and the one that causes real breaches — and no control at all against compromise. Only E4 holds against both, because the credential itself cannot address another tenant's data.

Correction: E3 layers on E2, it does not replace it. External practice treats application-layer and database-layer filtering as complementary. A service that dropped its choke point on reaching E3 would be worse off, since E3 fails open under injection. Claiming E3 therefore requires the E2 evidence artifact as well.

Correction: the GUC is set transaction-locally. Draft-2 said "at pool checkout", which is session scope and the wrong instrument. Under a pooler in statement mode, `SET` leaks between clients and returns other tenants' rows — a failure that appears only under production concurrency and produces no error. Use `SET LOCAL` inside an explicit transaction.

Platform enforcement is a platform obligation. Reaching E3 requires the storage platform to offer the mechanism: provisioned policies, a documented GUC contract, and a probe. Where a consumer wants E3 and the platform has not supplied it, the gap is the platform's. §19.6 asks `rapp-postgres` to define that contract, which must carry `FORCE ROW LEVEL SECURITY` on every tenant table (without it the table owner bypasses policies silently, and ADR-0001 already established that our migration role owns the tables it creates), no `BYPASSRLS` on leased roles, `SECURITY INVOKER` for ordinary logic, and an `EXPLAIN` comparison because RLS disables functional indexes built on non-leakproof functions.

Not all data is tenant-keyed, and the ladder must not pretend otherwise. A registry whose rows are the tenants has no per-tenant predicate to scope a policy by; enforcing one would break the service's function rather than secure it. `tenant-engine`'s `tenants` table is the worked example — `key-cape` enumerates it at token issuance and `flex-auth` queries it live, both of which are cross-tenant reads by design.

A service with mixed data shapes declares E-level plus a registry exception: the level its tenant-keyed tables hold, and a named list of tables excluded because they are registries rather than tenant data. The exception is part of the claim and is reviewable; an unnamed exception is an overclaim. Without this, mixed-shape services either overclaim or stay at E2 permanently, and `tenant-engine` declined to claim E3 on precisely that reasoning.

Default expectation for a new platform service: E2 at first serve, E3 recorded as target. Services whose cross-tenant exposure would be a reportable breach SHOULD target E3 or above.

### 4.4 Placement (P)

Placementaxis P
P0Shares a database with another consumer.
P1Database per consumer, shared cluster.
P2Dedicated cluster per consumer.
P3Dedicated cluster per tenant.
P4P3 plus separate region or jurisdiction.

Enforcement and placement are independent axes. Plotted together, with where each service actually sits — parenthesised entries are targets or defaults rather than current positions, and `—` marks a cell the coupling in §3.2 makes unreachable:

Enforcement →
E4
business app
E3
target
E2
tenant-engine
audit-core
E1
absorbed repo
E0
P0
P1
P2
P3
P4
Where a service sits todayTarget or defaultUnreachable at this placement

P0 → P1 → P2 is movement along the horizontal axis only. Those steps buy consumer isolation, capacity predictability, independent retention and a smaller operational blast radius. They do not raise the tenant boundary by one step. Only P3 makes E4 reachable. This is the most misusable fact in the framework and §11 governs how it may be described.

Decision 4.4.1: P1 is the default for platform services; P3 for client-facing business apps, as already ratified. A service unsure which it is must resolve that first (§19.4).

Decision 4.4.2 — placement scopes to data substrate. Identity-provider placement (realm-per-tenant versus Organizations) is the same silo/pool decision on a different substrate, is live in our estate, and is undecided. Realm-per-tenant carries a stated ceiling around 5–20 tenants, far below our target. Recorded here as a parallel question (§19.7), not folded into P.

### 4.5 Retention and erasure (R)

New in draft-3. Implemented abstractly by the storage platform for any dataset; policy is built on top of that interface by the consumer or its governance layer. Reference implementation: `rapp-postgres` ADR-0002.

Retentionaxis R
R0No retention or deletion position. Data kept indefinitely by default; no deletion path exists.
R1Platform default retention applies (N=30 days). The consumer has declared no requirement.
R2Retention declared as N days per dataset; the erasure horizon is published, and the consumer makes no promise shorter than it.
R3Policy-driven deletion: the consumer or its governance layer declares what is due, the platform sweeps whole datasets on that instruction and evidences each run.
R4Verified erasure: data proven unrecoverable across live storage, backups and derived copies, by one of the two routes below.

R4 has two routes and a service MUST name which one it uses.

Route | Mechanism | Cost
--- | --- | ---
Horizon-elapsed | Wait out the published erasure horizon; the data ages out of every retained copy. | Available to everyone, proves little, and the wait is set by a co-resident's retention requirement rather than your own.
Key-destroyed | Encrypt per entity, then destroy the key. Retained copies survive but are unreadable. | Requires per-entity keys, strong encryption, and an auditable destruction record. Immediate.

Decision 4.5.3 — key destruction is not sufficient on its own. The key-destroyed route requires that no retained commitment reveals the erased content. Found by `audit-core`, and it is a general defect rather than a fact about them:

- A SHA-256 over a canonical record whose fields are low-entropy — event type, actor, tenant, subject, timestamp — is a confirmation oracle. Anyone holding the hash can guess the payload, hash the guess, and confirm a match. Destroying the key does not make the content unrecoverable while that hash survives.
- Shreddability is not retrofittable onto an integrity chain that commits to cleartext. It has to be built as encrypt-then-hash at accept time, with the chain committing to ciphertext. Retrofitting means rewriting the chain — the exact thing a tamper-evident log exists to make detectable.

So a service claiming R4 by key destruction must show that its retained commitments — hashes, chains, indexes, search keys — do not reveal what was erased. The remedies are an HMAC under a per-subject key that dies with the key, or a per-record salt destroyed alongside it. `audit-core` cannot reach R4 under its current design and targets R2; a fleet R4 target must exempt it explicitly.

Regulatory standing of the key-destroyed route, stated carefully because overclaiming here is worse than anywhere else in this framework. Data protection authorities have accepted key destruction as erasure where physical deletion would be manifestly disproportionate, and the practice is recognised under conditions — strong encryption, irreversible destruction, and an auditable record of it. The EDPB has not formally endorsed it as Article 17 erasure. A service reaching R4 by key destruction is making a defensible claim, not a settled one, and must say so rather than reporting a clean "deleted".

Three further properties.

The erasure horizon is the interval between deleting data and it ceasing to be recoverable from anything the platform holds. Deleting a row does not remove it from yesterday's backup. With an N-day window, deleted data remains recoverable for N days. That is the difference between "deleted" and "erased" and the estate had never written it down.

On shared substrate, retention is not per-consumer. Physical backup is instance-wide — one WAL stream, one window — so the instance retention is derived as the maximum across co-resident consumers, and every consumer's horizon is that maximum. A consumer declaring 7 days beside one declaring 90 gets 90. This is the retention analogue of ADR-0001's blast-radius disclosure: state the coupling rather than imply an isolation that is not there.

Retention is therefore a placement trigger. A consumer needing a horizon shorter than the instance floor cannot have one at P1. It moves to P2 for a reason with nothing to do with performance — which is exactly why it needs recording, since nobody looks for a retention argument when reviewing placement.

Decision 4.5.4 — a retention promise binds both R and P. A tier making a retention claim records an R minimum and a maximum erasure horizon in days. It also requires P2 or above unless its provider contract guarantees that the shared-substrate horizon stays within that maximum and rejects or notifies before a co-resident change would extend it. A bare `R2` minimum is insufficient: at P1 another consumer can change the promise without changing the tier or its holder.

Deletion splits mechanism from policy. The platform deletes whole datasets on instruction and records an opaque policy reference it never interprets, so every deletion traces to what authorised it. Rows are not a dataset: row expiry is the consumer's own DML under its migration lease. Dropping a consumer's whole database is an operator-gated offboarding step, never a scheduled one.

### 4.6 Availability (V)

New in draft-8. `adaptive-pricing` found that §11 required availability claims to map to a minimum level while the framework supplied no availability vocabulary. Placement is not a substitute: a dedicated cluster can still be a single instance on a single node.

Availabilityaxis V
V0No availability or recovery position. Recovery is untested or depends on improvisation.
V1Restart or recreate recovery in one failure domain is documented and exercised. Interruption is expected; this is recovery, not failover.
V2Redundant instances provide automated service failover, with measured RTO/RPO; a shared failure domain or critical dependency may remain.
V3The complete critical path survives loss of one declared failure domain, with measured RTO/RPO from an exercise.
V4The complete critical path survives regional loss through tested multi-region failover, with measured RTO/RPO.

Decision 4.6.1 — V is end-to-end. A service declares the minimum across the components and synchronous providers required to serve the operation. An application with three replicas over a V1 database is V1. A status page or replica count is not evidence of a higher level.

Decision 4.6.2 — availability claims name the operation. A read-only degraded mode and a mutation path may have different V levels. Decision 5.2 applies: declare the paths and quote the minimum unless the customer-facing claim explicitly and unambiguously names the narrower operation.

## 05The posture vector

A service states one level per axis, plus a target, review dates, evidence and any exceptions. `current` is the highest evidenced level; a control present in code but still awaiting the evidence required by §13 goes in `implemented`, not in `current`:

    schema_version: "0.1"
    framework: netkingdom-tenancy-posture
    service: example-service
    role: tenant-data-service
    tenancy:
      current:     { I: 2, A: 3, E: 2, P: 1, R: 1, V: 1 }
      implemented: { E: 3 }
      target:      { I: 2, A: 3, E: 3, P: 1, R: 2, V: 2 }
      reviewed: "2026-08-17"
      review_due: "2027-02-17"
      service_class: interactive
      gap:
        E: "RLS is implemented; the §13 E3 probe is still absent."
        R: "Retention declared; erasure horizon not yet published to consumers."
        V: "Automated failover is not implemented or exercised."
    evidence:
      A3: "docs/evidence/authorization-denial.md"
      E2: "docs/evidence/cross-tenant-review.md"
      P1: "rapp-postgres/docs/evidence/isolation-2026-08-10.md"

Placement exceptions. Draft-2 assigned one P level per service, which cannot express the vertically partitioned model — most tenants pooled, some dedicated — that §11's isolation tiers require. A tier requiring `P2` bought by three tenants would put the service at two levels at once, forcing an over- or under-claim. Placement is therefore declared as a default plus exceptions:

      placement_exceptions:
        - tenants: ["tenant:enterprise:*"]
          P: 3
          reason: "isolation tier; see adaptive-pricing tier definition"

A service with exceptions must be able to say which tenants are on which substrate. That mapping is a first-class artifact, not archaeology.

Decision 5.5 — a provider declares what it makes reachable, not where it sits. The six ladders describe a consumer of infrastructure. They describe a provider of it badly, and `railiance-platform`'s review demonstrated how badly: `apps-pg` is `I0 A0 E0` because a database has no tenant concept, carries no tenant claim and applies no tenant predicate. Those zeros are structural, not weak — the cluster is exactly as strong as its consumers make it.

The sharp case is OpenBao at `E0`. Literally correct, and actively misleading: the mechanism in place is credential-scoped structural separation — `E4` machinery — pointed at a consumer boundary rather than a tenant one. A reader scanning a column of E values would rank it below a service doing per-query filtering in application code, inverting the real security position.

So a platform service additionally declares, per axis, the level available now, the maximum it can make reachable, and what a consumer must do to reach it. For `apps-pg`: E4 unreachable (shared credential per consumer, no per-tenant credential), E3 conditional on the GUC contract, R2 blocked on a backup target, V1 at most on the single-node rail. That is the sentence a consumer actually needs, and no arrangement of the consumer ladders produces it.

A provider's own `P` is `n/a`, not a number. `apps-pg` provides `P1`; it is not at `P1`, and writing `P: 1` there would later read as an isolation claim.

Decision 5.6 — enforcement stance is not a seventh axis, and `zones:` is reserved in this file. `zone-engine` asked whether enforcement stance — whether a given control is enforced, advisory or exempt in a given band of the estate — should fold in here rather than become a second standard. It should not, for a reason that is structural rather than territorial.

Every one of the six ladders is monotone: higher is stronger, and higher is what a service wants. That assumption is load-bearing throughout. §12's improve step moves a service up. §12's guard checks that none is below what it declared. §6 has to make a special allowance for a level that is permanently low by design, and §12's guard is told not to nag it — the allowance exists because low-is-normal is the exception here.

Enforcement stance is not monotone. The correct stance for a bootstrap lane is deliberately and permanently below the top rung, and the top rung is sometimes the wrong answer outright: `ops-warden`'s `ADR-0006` is exactly the finding that a fail-closed authorization gate on the SSH lane the tunnels depend on is not a stronger position, it is an outage. A ladder whose top is sometimes wrong is an enumeration, not a ladder, and putting one inside this vector would break `current`/`target`/`gap`, the guard, and §6 for the six that are.

§8.3 already refused an axis for a weaker reason than this one — that a QoS level would be an unenforced claim. Enforcement stance is enforced. It fails the other half of the same test.

There is a second, sharper reason. §6's conformance rule is accuracy, not altitude, and it works because this framework is descriptive: it never blocks anything by itself. Prescription enters only through §11 and Decision 8.2, where a requirer — never the declaring repo — sets a minimum level and the two are machine-reconciled. Enforcement stance is prescriptive by nature. Fold it in as an axis and an accurately declared `exempt` becomes conformant and exempt: a conformance rule that hands out the exemption it exists to audit. The declarer must not be the party that sets the stance.

So the split is the one `flex-auth` already argued to `zone-engine`: membership is data and is declared; stance is a rule and belongs to the control's owner. Membership is posture-shaped and behaves like a level. Stance behaves like a tier minimum under Decision 8.2 — asserted elsewhere, joined by machine.

What canon rules, and it is binding on the sibling standard:

- A security-zone standard is a separate document in this family, drafted by `zone-engine` and published in NetKingdom canon beside this one and the `*-engine` boundary contracts. It carries over §6 verbatim and §13's evidence discipline.
- Zone membership is declared in `tenancy.yaml`, under a reserved top-level `zones:` key, sibling to `tenancy:` and `provider:` — not inside `tenancy.current`. Decision 5.4 makes this file the repo's single posture declaration surface, and a second root file would recreate the divergence §5.4 was written to end. One file, one review cadence, one validator; two standards, because the two have different owners and different conformance semantics.
- `organization_posture` (`ops-warden` WP-0029) does not belong in this file at all, under either key. It is a fleet-wide, time-varying scalar describing the estate, not a property of the declaring service, and a per-repo copy of a global would go stale in as many places as there are repos. It is an input to stance selection and should be read by the zone model, not absorbed into a declaration.

Decision 5.6.1 — the workload remains the sole zone policy subject, and absence resolves to `unknown`. Here workload means an independently governed execution unit that performs application, automation, or operational control-plane work and can be attributed at enforcement time to both a responsible party and an authoritative workload identity. The independently governed executing unit behind operator-driven or automated SSH access, tunnel operation, credential brokering, policy compilation or enforcement, or maintenance is a workload when such a unit exists. The observed access, tunnel, grant, lane, pattern, or action is not itself a workload. A human or agent identity remains caller context; it does not replace the workload whose execution is being governed.

A repository, credential lane, grant template, pattern, or software package is not an alternative zone policy subject. A broker runtime is a workload; the grants and lanes it handles retain native resource identity and may explicitly be `not-applicable` to workload resolution. Every managed running deployable, including operational and tooling runtimes, has one authoritative `rapp-*/declarations/rapp.yaml` under RMGR-ADR-004. A pre-rapp managed runtime is migration debt and resolves `unknown`. An independently governed operational execution unit that is not a managed deployable may declare directly in its responsible repo's `tenancy.yaml`. The declaration surface broadens to cover real workloads; the subject model does not broaden merely to totalize an incomplete registry.

When a workload-applicable subject lacks an authoritative workload identity, when its reference is absent or ambiguous, or when that identity has no authoritative zone membership, the resolved membership MUST be `unknown`. It MUST NOT be inferred from a repository owner, credential path, lane type, actor class, environment, criticality, reef, organization posture, or a default zone. A control owner MAY define an explicit, reviewable fail-safe treatment for `unknown` — including denial, escalation, or a build-stage rule — but that treatment is stance, not membership. `unknown` never silently inherits a permissive zone or exception.

Decision 5.6.2 — zone membership requires an authoritative workload binding in the same declaration entry. A bare service name is a label, not identity evidence. Any single-service declaration carrying `zones:` MUST also carry `workload_identity`; in a layer repo using `services:`, both fields live on the same service entry. Top-level `zones:` is not valid for a multi-service file, because it would make membership ambiguous.

The binding states:

- `name` — the stable workload id, exactly equal to the declaration's `service`;
- `kind` — application, platform service, automation, operational control plane, or maintenance job;
- `responsible_repo` — the repository accountable for the identity and zone declaration;
- one or more `identity_bindings`, each naming the identity scheme, authoritative issuer or registry, exact subject, IAM Profile principal type, and optional environment and evidence; and
- for a managed deployable, a `declaration_ref` to its authoritative rapp declaration. Consuming catalogs reference it with the exact Repo Manager v1 tuple `(rapp_id, workload_identity.name)` and optional `deployable`.

An identity binding uses `principal_type: service` or `agent`. A human identity may still be required as actor or delegation context, but cannot be the sole workload identity. A managed application, operational runtime, or tooling runtime references its rapp declaration. Only an independently governed operational execution unit that is not a managed deployable declares directly in its responsible repo's `tenancy.yaml`; native actions and resources do not acquire a fictional workload or rapp merely to enter policy.

The stable workload id is the join key. Credential lanes, grants, controls, and compiled policy resources reference that id explicitly; compilers MUST NOT recover it by parsing a credential path or repository name. Multiple runtime principals may bind to one workload when environments or mechanisms differ, but the bindings must be unique and remain owner-reviewed. If no authoritative binding matches the runtime principal, Decision 5.6.1 returns `unknown`.

    service: ops-bridge-tunnel
    role: operational-access-path
    workload_identity:
      name: ops-bridge-tunnel
      kind: operational-control-plane
      responsible_repo: ops-bridge
      identity_bindings:
        - scheme: ssh-certificate
          authority: ops-warden
          subject: agt-ops-bridge
          principal_type: agent
          environment: prod
    zones:
      standard: security-zones_v0.1
      membership: z2-continuity
      responsible_party: ops-bridge
      justification: foundational tunnel path must retain availability under PDP loss
      context:
        maturity: M2
        criticality: high
        data_classification: confidential
      evidence:
        - ref: docs/evidence/ops-bridge-tunnel-zone.md
          supports: [M2, continuity-dependency, recovery]
      reviewed: "2026-08-22"
      review_due: "2026-11-22"

Worked examples after applying the evidence rule and minimum-across-paths rule consistently:

Service | Current | Notes
--- | --- | ---
`tenant-engine` | `I1 A0 E1 P n/a R0 V0` | Acting identity is caller-supplied; unauthorised read paths set the A minimum; E2-shaped child-table controls are not evidenced; SQLite is outside P; no erasure or availability evidence. This corrects draft-7, which quoted A2/E2 despite its own minimum/evidence rules.

`audit-core`

`I1 A2 E2 P1 R2 V0`

Bounded adversarial run `WH-ENG-20260822-AUDIT-E2-03` passed all three calibrated cross-tenant probes over ten operations, so E2 is evidenced as of 2026-08-22. This establishes only that the attempted attacks did not work. The 24-hour facility baseline requires review or replacement by `2026-08-23T22:10:25Z`. Its 30-day retention and erasure horizon remain declared and published.

`flex-auth` | `I1 A0 E1 P n/a R n/a V0` | Enables A3 for consumers. `/v1/check` authenticates no caller; E2 is implemented but not evidenced.
`platform-pg` (provider) | `I0 A0 E0 P n/a R2 V1` | Provides P1; backup/restore and single-node recovery are evidenced. Provides no tenant boundary by itself.
`apps-pg` (provider) | `I0 A0 E0 P n/a R0 V0` | Zeros are structural, except R0/V0 are live gaps: no backup and no recovery evidence.
`adaptive-pricing` observatory | `I0 A0 E0 P n/a R n/a V0` | Local, unauthenticated, single-user analysis surface; not a production service.
A newly absorbed repo | `I1 A1 E1 P0 R0 V0` | Conformant if declared, with a recorded path.

Decision 5.1: the posture vector is declared in the repo, not in the hub, consistent with local-files-are-source-of-truth.

Decision 5.2 — declare per path, quote the minimum. A service whose mutations are authorized and whose reads are not is at the reads' level. The quoted number is the minimum across paths; the per-path detail is declared beside it.

Draft-6 required only the minimum, on `tenant-engine`'s review. `audit-core` then showed why that is insufficient on its own: a bare minimum destroys signal, because `E3`-write/`E1`-read declares identically to `E1`/`E1`. Bare per-path invites "our write path is E3", which is the sentence §6 exists to stop. Both, related explicitly, is the rule.

Two services found this shape in themselves within a day of each other — `tenant-engine` (writes authorized, three read routes not) and `audit-core` (write path tenant-filtered, read path not filtered at all). Most services enforce harder on write than read, so this is the common case, not the corner.

Decision 5.3 — `n/a` is a level, and it is conformant. `P0` presupposes a shared database and `R0` presupposes retained data. A service holding nothing at rest — `flex-auth` runs with its registry and policy baked read-only into the image and no decision log persisted — is neither. A datastore outside a ladder's substrate vocabulary, such as `tenant-engine`'s current SQLite PVC, also uses `n/a` rather than inventing a level. Without an admissible `n/a`, a missing rung forces the fabrication §6 prohibits, which is precisely what draft-1 was rejected for. `n/a` is declared with a stated reason.

Decision 5.4 — the vector lives at `tenancy.yaml` in the repo root. Draft-6 said "in the repo" and not where or in what shape, which left §12's guard needing per-repo archaeology. `flex-auth` adopted `tenancy.yaml` speculatively; adopted here as the convention. A repo representing one service uses the single-service form above. A layer repo uses the schema's `services` list in the same root file — one vector per service, never an average. The normative schema is `canon/schemas/tenancy-posture_v0.1.schema.json`; prose documents may explain a declaration but do not replace it. The schema carries `current`, `implemented`, `target`, `reviewed`, `review_due`, `gap`, `placement_exceptions`, `service_class` (§8.3), per-path detail (§5.2), and provider reachability (§5.5), plus the workload identity prerequisite for zone membership (§5.6.2). It also permits `responsible_repo` for authoritative posture routing and `evidence_freshness` for machine-readable evidence observation, expiry, scope, owner, and remediation metadata. Their absence remains valid declaration syntax; feedback resolution must report `unknown`, never infer them from a directory, service name, or previous owner. From the `net-kingdom` repo, owners validate one or more declarations with `uv run tools/tenancy-posture/validate.py <path>...`; the validator applies the JSON Schema and the evidence, date, implemented/current and provider-range rules that JSON Schema alone cannot express.

## 06Conformance is accuracy, not altitude

A service is conformant when its declared posture is accurate, its target is recorded, and it does not claim a level it cannot evidence. It is non-conformant when it overclaims — at any altitude.

- Declaring `E0` is conformant. Concealing `E0` is not.
- A repo may be absorbed at any posture. It may not be absorbed silently.
- No service is blocked from the estate for being low on a ladder. Services MAY be blocked from specific work — serving a tenant grouping, holding a data class, carrying a plan tier — by requirements expressed as minimum levels.
- Downgrading is permitted and must be declared. A regression found by guarding is a defect; a regression declared in advance is a decision.
- A low level may be permanent by design, and the declaration must be able to say so. `flex-auth` is `I1` and always will be: a decision point evaluates the claims it is handed, and verifying its own inputs would make it the identity provider its scope refuses to be. A `target` equal to `current` with a reason is a settled position, not a stalled trajectory, and §12's guard must not nag it as though it were one.

Decision 6.1 — downgrades propagate. Before a planned downgrade of a current level or a provider's available level, the declaring repo MUST resolve the tier definitions and consumers that reference it. A downgrade below a recorded minimum blocks the change until the claim is changed, the workload is moved, or the affected owner explicitly accepts the gap. An unplanned regression is an incident and triggers the same notifications. Updating `tenancy.yaml` without notifying dependants is declaration drift, not a completed downgrade.

Without the axis separation, "not rigorous about tenant separation" is one verdict a repo passes or fails. With it, the same repo is `I1 A1 E1 P0 R0 V0` with a path — a plan, not an indictment.

## 07Portability across placement levels

Movement between P levels must be operational, not a rebuild:

- Connect by injected credential only — no cluster, host, namespace or database name in source.
- Own a whole database, never tables inside someone else's.
- Idempotent schema creation.
- No cross-database joins or co-location assumptions.

Decision 7.1: mandatory at P1 and above. At P3, SHOULD rather than MUST — a per-client instance that never moves is not misconformant for naming its own database.

## 08Placement triggers

Recorded at provisioning time: noisy neighbour on a latency-critical path; a compliance or residency requirement; a plan tier requiring a higher minimum; an erasure horizon that no longer fits (§4.5); connection or memory ceiling reached.

Decision 8.1: triggers MUST be monitored, not merely recorded. A trigger in a YAML comment nobody re-reads is documentation, not control.

Decision 8.2 — split authority, machine-reconciled. `railiance-platform` owns the placement rule; the package repo owns the substrate numbers and enforcement; the consuming repo owns its workload requirements; `adaptive-pricing` owns any tier minimum. `adaptive-pricing` declined a standing co-signature and the framework accepts the replacement: typed tier minima are joined to consumer and provider declarations at tier definition and whenever one changes. A machine-checkable constraint must not depend on somebody remembering to collect a signature.

Decision 8.2.1 — trigger monitoring has an owner. The provider monitors capacity ceilings and co-residency; the consumer monitors latency, compliance and erasure requirements; `adaptive-pricing` monitors tier-definition changes. The placement owner reconciles those signals. A trigger marked `unmonitored` is an explicit gap and cannot support a customer assurance claim.

### 8.3 Service class — a placement input, never a priority

A latency-critical consumer and a batch consumer can share an instance today with nothing distinguishing them. `tenant-engine` sits on `flex-auth`'s synchronous authorization path and chose a 5s statement timeout for that reason; `audit-core`, co-resident, is not latency-critical. Nothing prioritises between them.

The framework does not add a QoS axis, because the platform cannot enforce one. Community PostgreSQL has no resource governor: no per-role CPU or I/O priority, no resource queues, no workload classes. Those exist in EDB's enterprise variant, in Greenplum, and in SQL Server — not in what we run. A declared priority level would therefore be an unenforced claim sitting in a declaration, which is precisely what retiring `tenantIsolation` was about. An axis implies graduation and enforcement; this has neither.

Decision 8.3.1 — co-residents are equal. On shared substrate no consumer's query yields to another's. A consumer whose latency requirement cannot survive an unprioritised neighbour must escalate to `P2`. That is the honest mechanism and it is the only one we have.

Decision 8.3.2 — service class is declared anyway, as a category rather than a level: `latency-critical`, `interactive`, or `batch`. It buys three things, none of which is priority:

- A placement input. Mixing `latency-critical` with `batch` on one instance is a recognised mismatch. It may still be the right call — it is right today — but it should be a decision, not an accident of who was provisioned when.
- A trigger. A `latency-critical` consumer acquiring a `batch` co-resident is a recorded placement trigger under §8, on the same footing as noisy neighbour.
- An acceptance criterion for evidence. The noisy-neighbour artifact in §13 asks whether measured degradation is acceptable; without a declared class that word has no referent. Degradation tolerable for `batch` may be an outage for `latency-critical`.

Decision 8.3.3 — class mixture must be visible. The platform reports which classes are co-resident. An unenforceable risk that nobody can see is strictly worse than one that is stated.

### 8.4 Substrate location is not evidence — and reefs are not reconciled with `P` or `V`

Decision 8.4.1 — location is not evidence of any property of the workload on it. Three repos have now written this rule locally in three vocabularies: `railiance-master`'s topology is not readiness, `zone-engine`'s placement is not posture, and §3.1 here, which requires every use of "isolation" to name its axis. They are one rule. Stated once: the substrate a workload sits on is never, by itself, evidence for a level on any ladder in this framework. Binding to a reef, being on a dedicated instance, or naming a rail proves placement and nothing else. Other repos should cite this rather than restate it.

Decision 8.4.2 — the reef taxonomy and this framework are not reconciled, and that is this document's defect. `zone-engine` asked whether canon should reconcile reefs with the posture axes, on the assumption that this was a scope question for the zone model. It is not: the unreconciled pair is not zone ↔ reef, it is reef ↔ `P` and `V`, and it belongs to canon.

`repo-manager` owns substrate placement (`reef-railiance`, `reef-storage`) with an explicit residual-risk acceptance attached to a binding. §7 and §8 of this document presuppose that placement is fully described by the `P` ladder. It is not. `P` grades tenant data isolation within a datastore; a reef is a named compute substrate carrying an accepted residual risk. The `P` ladder has no rung meaning "single node, shared control plane, risk accepted", and inventing one would be the fabrication §6 prohibits.

A reef binding therefore satisfies none of the `P` couplings in Decision 3.2 by itself. It records the compute substrate and its accepted residual risk; it does not establish a database-per-consumer or database-per-tenant boundary, a per-tenant credential or encryption boundary, or an erasure horizon. Those remain properties of the workload and data provider declarations. The binding does participate in `V`, because availability composes across the complete critical path and the substrate can impose a ceiling.

The live consequence is on `V`, not `P`. §4.6 already warns that "a dedicated cluster can still be a single instance on a single node", and Decision 4.6.1 makes `V` the minimum across the synchronous path. `reef-railiance` is single-node with a shared control plane, which caps `V` for every workload bound to it regardless of that workload's own replica count — which Decision 4.6.1 already says is not evidence. Nothing today joins the reef's facts to a consumer's `V` declaration, so a rapp can declare `V2` accurately by its own reading and be wrong by this document's own composition rule.

This is `railiance-platform`'s provider-declaration finding (Decision 5.5) generalised one layer down. A reef is a provider and has nowhere to state what it makes reachable. Tracked as `NK-WP-0027`; the fix is canon's, and the zone model is not blocked on it.

The known escalation short of P2 is gateway-level prioritisation — ordering submissions in a connection proxy by the requesting tenant's current consumption. It is real, it is where the industry puts this when it must, and it is new infrastructure we do not run. Recorded as the option, not adopted.

## 09Credentials as a tenancy control

Short-lived leased credentials re-read at connection checkout, with overlap-first rotation, bound the residual risk at every E level below E4: a leaked credential expires rather than persisting. Stronger than the industry norm of a long-lived per-service secret.

Decision 9.1: static long-lived database credentials are not a sanctioned path for any service above E0.

Decision 9.2 — the rule extends to consumer-facing credentials. Draft-6 named database access only. `audit-core` pointed out that its ingest credentials are static long-lived bearer tokens, rotated by publishing a second alongside the first — and that the argument applies with more force to the credential that actually carries the tenant claim than to the one that reaches the database behind it. Read as an accidental omission; it was. Consumer-facing credentials are named in. Where a service cannot yet meet this, it is a stated gap rather than a silent exclusion.

## 10Blast radius must be published

Decision 10.1: every platform holding consumer data MUST publish, in concrete terms, what a leaked runtime credential can and cannot reach at the levels it operates. `rapp-postgres` ADR-0001 §5 is the reference. Where the model cannot provide a guarantee, the platform says so and names the escalation.

Decision 10.2 — quotas are disclosed, not discovered. The same obligation extends from what a leaked credential can reach to what the platform will refuse to do for you. Every consumer MUST be told, at provisioning, the throttles and quotas enforced against it — connection limits, statement timeouts, idle-transaction timeouts — and told again when they change. A consumer learning its statement timeout by hitting it in production is a disclosure failure, not a consumer bug. This is how `tenant-engine` was provisioned, by good practice rather than by rule; the rule now exists.

## 11Commercial expression

- 11.1 Plan tiers are expressed internally as typed assurance requirements. A tier may require `E3 P2 R2 V2` and a maximum erasure horizon; it need not print those labels anywhere customer-facing.
- 11.2 Marketing and product language is free. No requirement to expose level labels or this document. "Dedicated infrastructure", "isolated tenancy", "private instance" all remain available.
- 11.3 The constraint is on evidence, not vocabulary. A customer-facing isolation, availability or retention claim must map to a minimum level the delivering service actually holds, recorded once when the tier is defined. The review is internal and happens at tier definition — not per campaign.
- 11.4 Two hard lines, because these reach contracts and compliance questionnaires:
- A claim that another tenant cannot reach the customer's data requires E4.
- A claim that deleted data is gone requires R4, or an erasure horizon disclosed alongside it. Where R4 is reached by key destruction, the claim is defensible but not settled law (§4.5) — it may be made, and it may not be made in language that implies a regulator has blessed it.
- A claim that service survives loss of a zone requires V3; regional-loss language requires V4. "High availability" without a named failure and measured recovery objective is not an assurance claim this framework can evidence.
- 11.5 — sanctioned honest language. The strong prohibitions above must not leave a commercial writer with only silence:
- E3 may be described as database-backed defence against an omitted tenant filter; it must not be paraphrased as "another tenant cannot reach".
- P2 may be described as a dedicated service database cluster with an independent capacity and restore boundary; it is not tenant-dedicated.
- R2 may state the declared retention and published erasure horizon.
- V1 may state exercised restart recovery in one failure domain and must say that interruption and single-domain loss remain.
- 11.6 — authority and reconciliation. The tier definition is authoritative for the minimum and customer wording. `tenancy.yaml` is authoritative for the delivering service's current level; provider declarations are authoritative for what infrastructure makes available. None is derived by copying another. Approval joins them and fails closed on a missing, stale or insufficient declaration. A performance-differentiated tier requires P2 or an enforceable resource governor; service class alone grants no priority.

## 12Methodology — analyze, establish, improve, guard

Analyze. Assess a repo against the ladders; produce `tenancy.current` with reasoning recorded. Applies to new and absorbed services alike.

Establish. Declare the target and gap. The target is set by data class, tenant groupings served and plan tiers carried — not by ambition.

Improve. Move one axis at a time. Raising P while leaving E untouched is the characteristic misstep.

Guard. Verify continuously that the declared posture holds — against the service's own declaration, not a universal maximum. Nobody must prove every service is at E4; the check is that none is below what it declared.

Regression found by guarding is a defect; regression declared in advance is a decision. The estate has been bitten twice by silent pin rollbacks producing ordinary-looking 403s and 404s rather than errors. Posture regression looks the same — an RLS context leak returns correct-looking rows for the wrong tenant. Guarding must be designed for invisible failure, not for crashes.

## 13Evidence per level

Decision 13.1: a current level is claimed only with its evidence artifact present. This turns §6's accuracy rule from an honour system into a check. `implemented` records a control observed in code or configuration whose required artifact is still absent; it never satisfies a tier minimum.

Decision 13.1a — the floor needs no artifact, only a reason. Found independently by `audit-core` and `flex-auth`: the table below defines artifacts from `I2`, `A2`, `E1`, `P1`, `R2` upward and none below, so a literal 13.1 made the lowest rungs unclaimable — including §5's own worked example of a conformant absorbed repo, `I1 A1 E1 P0 R0`, which could not satisfy it on any axis. A rule that forbids the declaration §6 exists to permit is a defect in the rule.

At `I0/I1`, `A0/A1`, `E0`, `P0`, `R0/R1`, `V0`, or `n/a`, a declaration requires a stated reason, not an artifact. Evidence is what stops you overclaiming, and there is nothing to overclaim at those floors.

Decision 13.4 — an artifact must assert something achievable. Draft-3's noisy-neighbour evidence required proof that a saturating consumer "does not breach" another's allowance. Shared infrastructure cannot provide that; the risk is inherent and cannot be wholly removed. An artifact that can only fail, or that passes by being run gently enough, is an overclaim wearing the costume of evidence. Where a property cannot be guaranteed, the artifact measures and records it instead.

Decision 13.2 — evidence is of two kinds, and conflating them is an overclaim. Mechanical evidence is a structural assertion a machine can make and belongs in CI. Adversarial evidence is semantic, requires setting up separate tenant contexts and comparing responses, and carries a review date rather than a green build. Cross-tenant findings are the category external testing practice identifies as needing human review. A passing CI run is not E2 evidence.

Decision 13.5 — freshness and ownership are explicit inputs. A declaration may attach `evidence_freshness.<level>` to an evidence key. An adversarial entry requires `observed_at`, `valid_until`, `responsible_repo`, `scope`, and `remediation`; a mechanical entry may omit `valid_until` when the artifact is continuously re-established by the referenced revision or CI control. The timestamps use RFC 3339 and the responsible repository is the authority for replacement evidence.

The feedback evaluator does not parse prose for dates, infer ownership from a file path, or silently extend a validity window. A current adversarial claim without freshness metadata resolves to freshness `unknown`. An expired artifact resolves to freshness `expired`. Neither automatically rewrites the declared level: the evaluator emits a deterministic owner-routed remediation proposal so review remains observable and controlled. The proposal contract is `posture-feedback_v0.1`; it performs no State Hub write or policy mutation.

Level | Evidence | Kind
--- | --- | ---
I2 | Identifiers validated against the vocabulary; rejection test for a malformed id; binding shown to come from a verified token | Mechanical
I3 | Live re-query demonstrated on an `aal2`-class path; cached-claim path shown unused there | Mechanical
A2 | Choke point identified; test that an unbound request is refused | Mechanical
A3 | Live decision with a denial observed at the endpoint, not only at the decision surface | Mechanical
A4 | Decision served over the standard interface; a second PDP substituted without PEP change, with the decision differences between the two recorded — substitution proves interface portability, not decision equivalence | Mechanical
E1 | Every tenant-owned table carries the tenant key | Mechanical
E2 | Choke point identified; identity bound to tenant A demonstrably cannot read tenant B | Adversarial, with a review date
E3 | `FORCE ROW LEVEL SECURITY` on every tenant table; no `BYPASSRLS` on leased roles; probe that a session without the GUC reads nothing; probe that a wrong GUC reads nothing; `EXPLAIN` comparison | Mechanical
E4 | Per-tenant credential demonstrated unable to connect to another tenant's substrate | Mechanical
P1–P4 | Provisioning declaration plus the platform's isolation probes | Mechanical

Shared P1–P2 capacity assurance

A recorded baseline of per-consumer resource usage; a run in which one consumer saturates its declared allowance; evidence that the governance controls bind (the greedy consumer is held at its limits) and that the degradation co-residents experience is measured, recorded and judged acceptable against each one's declared service class (§8.3); the aggregate headroom at time of measurement

Adversarial, load-generated, with a review date

R2 | Declared retention rendered; erasure horizon published and reported in the operator surface | Mechanical
R3 | Sweep evidence records: timestamp, dataset, identifiers removed, authorising policy reference | Mechanical
R4 | Erasure demonstrated across live data, backups and derived copies within the horizon | Adversarial
V1 | Critical dependencies enumerated; restart/recreate recovery exercised; interruption and measured recovery time recorded | Mechanical exercise
V2 | One instance terminated while traffic continues or recovers automatically; measured RTO/RPO and remaining shared failure domains recorded | Adversarial, failure-injected
V3 | Declared failure domain removed in an exercise; complete critical path and degraded modes observed against RTO/RPO | Adversarial, failure-injected
V4 | Region made unavailable in an exercise; traffic and state recover in the alternate region against RTO/RPO | Adversarial, failure-injected

The P1–P4 artifact proves the declared placement topology. The shared-capacity artifact is additional: it is required before a P1/P2 service can claim that a noisy-neighbour control binds, that the trigger is actively guarded, or that a customer performance assurance survives co-residency. It is not required merely to report the true topology as P1 or P2. No such capacity artifact exists in the estate today, so §11 requires P2 or an enforceable governor for a performance-differentiated tier.

Decision 13.3: the tenant-boundary E2/E3 and noisy-neighbour artifacts do not exist anywhere in the estate today. `rapp-postgres` runs 19 adversarial probes, all against the consumer boundary, none against the tenant boundary inside a consumer. Externally, what this framework calls a tenant boundary failure is Broken Object Level Authorization — OWASP API1, top of the API Security Top 10 since that list launched, and the most commonly exploited API vulnerability in published assessments. We have no coverage for the highest-ranked risk in our class of system. §19.3 records the owner.

## 14Adoption stance — structure, not tooling

Decision 14.1: external research is design input. This estate adopts published standards and structural patterns; it does not adopt tooling unless that tooling is an established industry standard with broad application. Everything else is built ground-up, so it can be optimised and refactored as the estate sees fit.

Class | Stance
--- | ---
Security baselines (OWASP Multi-Tenant Security Cheat Sheet, API Security Top 10) | Adopt as the external reference our ladders answer to
Standards bodies (OpenID AuthZEN 1.0) | Adopt — this is what A4 is
Reference taxonomies (Azure tenancy models, AWS SaaS Lens, cell architecture) | Adopt as structure
Engine behaviour (PostgreSQL RLS mechanics) | Facts, not tooling
Third-party analyzers and test frameworks | Do not adopt. Take their rule taxonomies as checklists for probes we write ourselves

The practical effect is small and good: `rapp-postgres` already owns a ground-up probe harness — bash and psql, no dependency tree — that found four real defects in its own provisioning SQL. The evidence artifacts in §13 become new probes in a tool we control. One idea worth reimplementing from the external survey is policy-diff classification: labelling a change to an enforcement policy as safe or breaking before it lands.

## 15Alternatives considered

One fixed model with a single set of characteristics (draft-1). Rejected: cannot describe a repo that is not there yet, forcing absorbed repos to misrepresent their posture or stay outside. A framework that can only describe its own end state is not a framework.

A maturity model with a single overall level. Rejected: collapses the axis separation. A service strong on identity and weak on enforcement has a specific, actionable gap; one composite score hides it and invites averaging.

Prohibiting row-level security (draft-2's inherited position). Rejected in draft-2, refined in draft-3: RLS is a real rung against the common threat. The error was never RLS — it was describing E3 in E4's language.

Schema-per-consumer in one database. Rejected: `pg_catalog` is readable per-database, so every co-resident enumerates every other's table and column names regardless of grants. Retained as a describable state, never a target.

Mandating E4 for everyone. Rejected: the tenant taxonomy includes `consumer` (private individuals) and `family`. A cluster per private individual is economically impossible; the taxonomy is itself evidence pooling is required.

Per-consumer physical backup retention. Rejected: CNPG retention is a property of the instance's WAL archive. There is no mechanism, and claiming it would be a fabricated guarantee. Hence the derived maximum in §4.5.

Platform-scheduled row expiry. Rejected: requires the platform to hold DML authority over consumer schemas and interpret consumer data semantics, both forbidden by ADR-0001. The consumer's migration lease is the correct instrument.

Leaving each repo to its own model. Rejected: the status quo, which produced two contradictory ratified defaults and an unowned placement question.

## 16Held against outside practice

The graduated reframe is corroborated, not invented here. Microsoft's tenancy-model guidance states it almost verbatim: "Instead of viewing isolation as a discrete property, consider it a spectrum. You can deploy components of your architecture that are more isolated or less isolated than other components in the same architecture." The same guidance derives our E↔P coupling independently — shared deployment means enforcement lives in application code; dedicated deployment means it is structural.

Stronger than typical. Most multi-tenancy literature models one boundary, tenant-to-tenant. This estate has two stacked boundaries: platform-service to platform-service, and tenant to tenant inside a consumer. Naming them separately and refusing to enforce both with one mechanism is uncommon and correct. Graduated per-axis levels also beat the silo/pool/bridge trichotomy, which is approximately our P axis with the other four missing — which is why it cannot express "pooled infrastructure, structurally enforced boundary".

Weaker than typical. The pool model's standard mitigation is a verified enforcement layer every service is demonstrably routed through. We have the concept and none of the verification (§13.3).

Adopted without naming it. Short-lived leased credentials re-read at checkout beat the long-lived-secret norm. §9 promotes it to a tenancy control.

Still unexplored. Neither P nor R describes a cell — a slice of infrastructure with a fixed maximum size, sized so one cell's failure is survivable and cell count scales linearly. `platform-pg` is, in these terms, an uncapped cell: §17 computes a ceiling and nothing enforces it (§19.8).

Sources: the five research digests in `the-custodian/research/2026-08-17-adr008-*`, which carry full citations for the claims in this section.

## 17Scaling demands

Derived from the live `platform-pg` specification. Connection arithmetic is exact; the per-backend memory estimate remains unmeasured and is explicitly a gap in `rapp-postgres` ADR-0004.

    instances:        1              (no HA; single-node rail)
    max_connections:  100
    memory limit:     1Gi
    per consumer:     14 connections (12 runtime + 2 migration)

The hard connection bound is roughly six declarations; the enforceable operational ceiling is four. Seven declarations request 98 of 100 connections before CNPG's instance manager, metrics exporter and reserved slots. Every one of them is politely inside its declared 14-connection allowance; the instance still fails. ADR-0004 sets four because memory is expected to bind first and fails by OOM-killing every co-resident rather than refusing one connection.

That distinction matters because our governance addresses the wrong shape. Per-consumer `connection_limit`, `statement_timeout` and `idle_in_transaction_session_timeout` guard well against one greedy consumer. They do nothing about the aggregate of many modest ones, which is the second and less intuitive noisy-neighbour failure and the one this number describes. Two workload consumers plus the isolation probe occupy three of the four declared slots. The next workload request must trigger measurement and the overflow decision before admission.

Memory likely binds first. 100 backends against 1Gi is ~10MB per backend. Connection exhaustion errors clearly; memory pressure OOM-kills and degrades every co-resident at once.

E3 and pooling. Corrected from draft-2, which had this backwards. Transaction-scoped context (`SET LOCAL` inside an explicit transaction) is what makes E3 safe under a pooler. Statement-level pooling is what breaks it, serving other tenants' rows under concurrency with no error. E3 constrains which pooling mode is available, not whether pooling is available.

Retention consumes the volume. WAL accumulates with the window, and §4.5 makes the window the maximum across consumers. A consumer declaring a long retention extends everyone's horizon and everyone's storage draw against a 20Gi volume.

Restore time couples all consumers. Physical backup is instance-wide, so a consumer's RTO is a function of total instance size, not its own.

`platform-pg` is V1. `instances: 1` on a single-node rail provides exercised restart recovery and no failover. P1 describes its consumer placement and says nothing about this availability fact; the new V axis carries it.

## 18Consequences

- The estate gains one vocabulary and a way to be honest about partial adoption.
- Absorbed repos get a described state and a path instead of a failing grade.
- `tenantIsolation` in `PostgresConsumer` is revealed as a mislabelled field.
- The verification problem becomes tractable: guard against declaration.
- Draft-2's RLS prohibition is reversed and its E3 description corrected; `rapp-postgres` acquires an obligation to define and offer the mechanism.
- Adding a consumer with long retention silently extends everyone's erasure horizon. This must reach the consumer review checklist, not only this document.
- A service selling an isolation tier must maintain a tenant→substrate mapping it does not have today.
- Availability becomes an end-to-end, evidenced property rather than an inference from replica count or placement.
- Nothing here changes a running system.

## 19Review resolutions and residual questions

1. `tenantIsolation` field — resolved. `rapp-postgres` retired it. A consumer declaration asks for mechanisms; posture lives in the consumer's `tenancy.yaml`.
2. Placement ownership — resolved. §8.2 records the split. The policy has one owner; typed tier requirements replace the declined commercial co-signature.
3. E2, E3 and noisy-neighbour evidence — owned as of 2026-08-17 by `whitehat-security` (WHITEHAT-WP-0001), an independent adversarial evidence facility seeded for this purpose. `audit-core` and `tenant-engine` were right to decline it as fleet-scope work; the answer was a home of its own rather than a volunteer.

Owned by NetKingdom — corrected 2026-08-17; an earlier revision of this section proposed otherwise on independence grounds and was overruled. Offensive security is security work and belongs with the repo that owns security. The facility is framed offensively rather than as a conformance checker: it is pointed at infrastructure we choose, our own estate among them, and conformance testing is one use of a general capability.

The residual tension is recorded rather than resolved: NetKingdom owns this framework and the facility that tests conformance to it, so those findings are NetKingdom assessing NetKingdom. The mitigation is that findings leave for `risk-nexus`, under `the-custodian`, rather than being closed in place. Proportionate, not perfect. Revisit if conformance findings start getting quietly closed.

Two consequences land back here. Cadence is now a security parameter, not a schedule — for any control whose guarantee is detection rather than prevention, the interval between probe runs is the exposure window, and `rapp-postgres` ADR-0003 leaves that number to the facility. And a passing suite is not proof of isolation; it is proof that the attacks attempted did not work. §13's evidence artifacts should be read with that distinction, because a green run recorded as "E2 verified" would be exactly the overclaim §6 prohibits.

1. Business app vs platform service — open. Custodian canon: a classification rule. Candidate: reuse `repo-classification-standard_v1.0`.
2. Tier → minimum level mapping — policy resolved, implementation open. `adaptive-pricing` owns typed minima and wording; `tenant-engine` owns plan assignment by id. Current tiers make no assurance claims.
3. The E3 mechanism — resolved. `rapp-postgres` ADR-0003 publishes the GUC contract with the `FORCE`/`BYPASSRLS`/`SECURITY INVOKER`/`EXPLAIN` requirements.
4. Identity-provider placement — open. Owner of `key-cape`: realm-per-tenant or Organizations? Realm-per-tenant's ~5–20 tenant ceiling is below our target.
5. Cell sizing — resolved for `platform-pg`. `rapp-postgres` ADR-0004 sets four consumers and names absent overflow target `platform-pg-2`; measurement and provisioning remain live gaps.
6. Retention floor and ceiling — resolved as policy. Both exist; requests outside them fail validation and the package repo owns the numbers.
7. Engine neutrality — open. The P ladder rests on a PostgreSQL property. State it engine-specifically and say so, or abstract it and risk a non-Postgres implementation that silently differs?
8. Erasure versus audit — framework resolved. `audit-core`: crypto-shredding a tenant's audit records destroys the evidence the service exists to hold, and ADR-0001 §2 deliberately built the role model so history could not be rewritten. The usual resolution separates the fact of an event, retained, from its personal payload, encrypted per subject and shreddable. Raised because a naive "R4 everywhere" target would instruct the audit service to destroy its own evidence. `audit-core` targets R2 and is explicitly not a fleet R4 target. The legal basis for retaining audit facts remains a risk/legal question outside this framework.
9. Quality of service — resolved 2026-08-17. Co-residents are equal; a declared service class informs placement but never grants priority. See §8.3. The question asked whether to add a QoS dimension; the answer is no, and the reason is that we could not enforce one.

Tenant grouping ambiguity — resolved outside this framework. ADR-0013 revision 2 makes the identifier segment immutable onboarding-time history and the `tenant-engine` record authoritative for current grouping. Consumers do not parse current policy or spend-ceiling inputs from `tenant:<grouping>:<name>`.

## 20Ratification path

1. Reviewed by `tenant-engine`, `flex-auth`, `audit-core`, `rapp-postgres`, `railiance-platform` and `adaptive-pricing` against §19. Complete in draft-8.
2. Each publishes its own posture vector (§5) as part of review. The framework is validated by whether it can describe them accurately — if a repo cannot express itself in these six ladders, the ladders are wrong and this document changes, not the repo. Complete in draft-8; all six root declarations validate against the canonical schema.
3. On acceptance, supersedes the routing of `rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1-draft.md`, whose §§3–8 are absorbed here. That draft is withdrawn rather than left pending.
4. On acceptance, `rapp-postgres` ADR-0001 through ADR-0004 move to `accepted` and are annotated as the PostgreSQL implementation of the E, P, R and shared-capacity rules.