`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.**
**Decision 3.3 — scope.** The P and R ladders describe a service's **primary
datastore**. Caches, search indices, message queues and background jobs are
named leak surfaces in the external baselines and are assessed separately, not
covered by a posture vector. Saying so is honest; implying the vector covers
them would not be.
## 4. Graduated 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)
| Level | State |
|---|---|
| **I0** | No tenant concept. Data not attributable to a tenant. |
| **I1** | A local tenant notion exists but is not canonical, **or** the tenant is taken from the request rather than from a verified token. |
| **I2** | Canonical identifiers, bound at the identity provider and carried as a verified claim; `tenant-engine` is the source of existence. |
| **I3** | I2 plus capability roles honoured, with live `tenant-engine` re-query for privileged, destructive, credential-vending or `aal2`-class decisions. |
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.
`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)
| Level | State |
|---|---|
| **A0** | No authorization, or tenant context not carried. |
| **A1** | Ad-hoc checks scattered through handlers. |
| **A2** | A single local authorization boundary; tenant context bound once, centrally. |
| **A3** | Decisions delegated to `flex-auth` as PDP, with live re-query where the IAM Profile requires it. |
| **A4** | A3 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. |
A4 is new. `flex-auth` uses a bespoke `CheckRequest` and a bespoke action
vocabulary, with action strings copied verbatim between repos to avoid
re-derivation — exactly the coupling AuthZEN removes. The specification reached
Final in January 2026 and Keycloak shipped experimental support in May. We are
not wrong, we are pre-standard, and the ladder should have somewhere to go.
**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 — `flex-auth` calls `tenant-engine`
synchronously on the authorization path. A service identity acting on behalf of
a tenant must carry and revalidate tenant context to claim A2 or above.
### 4.3 Enforcement (E)
| Level | Mechanism |
|---|---|
| **E0** | None. Data not tenant-keyed; separation incidental or absent. |
| **E1** | Data tenant-keyed, filtering applied per query at call sites. |
| **E2** | Filtering centralised at a single service-side choke point binding authenticated identity to permitted tenants. |
| **E3** | E2 **plus** platform-assisted filtering: row-level security keyed on a tenant GUC set transaction-locally, or an equivalent enforced data-access layer. |
| **E4** | Structural: 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 | ✗ | ✗ | ✓ | ✓ |
| **R0** | No retention or deletion position. Data kept indefinitely by default; no deletion path exists. |
| **R1** | Platform default retention applies (N=30 days). The consumer has declared no requirement. |
| **R2** | Retention declared as N days per dataset; the **erasure horizon** is published, and the consumer makes no promise shorter than it. |
| **R3** | Policy-driven deletion: the consumer or its governance layer declares what is due, the platform sweeps whole datasets on that instruction and evidences each run. |
| **R4** | Verified 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. |
**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.
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.
## 5. The posture vector
A service states one level per axis, plus a target, a date, and any placement
exceptions:
```yaml
tenancy:
current: { I: 2, A: 3, E: 2, P: 1, R: 1 }
target: { I: 2, A: 3, E: 3, P: 1, R: 2 }
reviewed: "2026-08-17"
gap:
E: "Choke point exists and is tested; RLS not provisioned. Blocked on
rapp-postgres publishing the GUC contract. Target Q4."
R: "Retention declared; erasure horizon not yet published to consumers."
```
**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:
```yaml
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.
Worked examples, best-effort and subject to owner correction:
| Service | Current | Notes |
|---|---|---|
| `tenant-engine` | `I2 A3 E2 P1 R1` | Moving to P1 under TEN-WP-0009; retention declared, horizon not yet published. |
| `audit-core` | `I2 A3 E2 P1 R1` | Holds audit evidence, so both E3 and R2 are urgent targets. |
| A newly absorbed repo | `I1 A1 E1 P0 R0` | 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.
## 6. Conformance 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.
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` with
a path — a plan, not an indictment.
## 7. Portability 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.
## 8. Placement 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:** placement policy ownership is proposed to
`railiance-platform`, **co-signed by `adaptive-pricing`**. Tenancy model
selection is a commercial decision as much as a technical one; an
**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.
## 13. Evidence per level
**Decision 13.1:** a level is claimed only with its evidence artifact present.
This turns §6's accuracy rule from an honour system into a check.
**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.**
| 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 | 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 |
| **P1–P2 (noisy neighbour)** | 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 |