ADR-008 draft-2: reframe from fixed model to graduated framework
Draft-1 proposed one model of multi-tenancy with fixed characteristics. Rejected: the estate needs a framework that can hold several situations at once, including repos that do not separate tenants rigorously today and must be developed toward doing so. What changed: - Every plane now carries an ordered ladder (I0-I3, A0-A3, E0-E4, P0-P4), not just placement. A service is a posture vector, not a verdict. - D3 reversed. Draft-1 forbade RLS as a control that "looks like a database guarantee without being one". The observation was right, the conclusion wrong: RLS is E3, materially stronger than E2, and the actual error was calling E3 by E4's name. Platform enforcement is now the direction of travel and an obligation on the platform, not only on consumers. - New governing rule: conformance is accuracy, not altitude. Declaring E0 is conformant; concealing it is not. Overclaiming is the only violation. - Fixed a flaw of draft-1's own making: R0 "shared tables, tenant column" was an enforcement state mislabelled as placement. Removed; P ladder renumbered. - Added methodology (analyze/establish/improve/guard) and per-level evidence artifacts. Guarding checks a service against its own declaration, which is what makes the verification problem tractable at all. - D7 softened per operator: tiers carry minimum levels internally, marketing language stays free, the constraint is on evidence not vocabulary. One hard line kept: "another tenant cannot reach your data" requires E4. Ratification now also tests the framework — each reviewing repo publishes its posture vector, and if a repo cannot express itself in these ladders the ladders are wrong, not the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f8038ecde5
commit
f8c3c1d8fd
1 changed files with 444 additions and 272 deletions
|
|
@ -1,25 +1,31 @@
|
||||||
---
|
---
|
||||||
id: ADR-008
|
id: ADR-008
|
||||||
type: architecture-decision-record
|
type: architecture-decision-record
|
||||||
title: "Multi-Tenancy Model: Four Planes, One Isolation Ladder"
|
title: "Multi-Tenancy Framework: Four Planes, Graduated Levels, Declared Posture"
|
||||||
status: proposed
|
status: proposed
|
||||||
decided_by: Bernd Worsch
|
decided_by: Bernd Worsch
|
||||||
date: "2026-08-17"
|
date: "2026-08-17"
|
||||||
tags: ["architecture", "multi-tenancy", "isolation", "placement", "tenant-engine", "flex-auth", "rapp-postgres", "scaling"]
|
revision: "draft-2"
|
||||||
|
tags: ["architecture", "multi-tenancy", "isolation", "placement", "maturity", "tenant-engine", "flex-auth", "rapp-postgres", "scaling"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-008: Multi-Tenancy Model — Four Planes, One Isolation Ladder
|
# ADR-008: Multi-Tenancy Framework — Four Planes, Graduated Levels, Declared Posture
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Proposed.** Drafted 2026-08-17 at the operator's request. This document
|
**Proposed, draft-2.** Draft-1 (2026-08-17) proposed a single model with fixed
|
||||||
decides things currently owned by `tenant-engine`, `flex-auth`,
|
characteristics. It was rejected on the grounds that the estate does not need
|
||||||
`rapp-postgres`, `adaptive-pricing`, and `railiance-platform`. It must be
|
one model of multi-tenancy — it needs a framework able to describe, hold, and
|
||||||
reviewed by those owners before ratification; §12 lists what each is being
|
improve several situations at once, including repos that do not separate
|
||||||
asked to accept. Until ratified it governs nothing, and the existing
|
tenants rigorously today and must be developed toward doing so. Draft-2 is
|
||||||
per-repo documents remain authoritative for their own repos.
|
that reframe.
|
||||||
|
|
||||||
## Context
|
This document decides things currently owned by `tenant-engine`, `flex-auth`,
|
||||||
|
`rapp-postgres`, `adaptive-pricing`, and `railiance-platform`. It must be
|
||||||
|
reviewed by those owners before ratification; §18 lists what each is being
|
||||||
|
asked to accept.
|
||||||
|
|
||||||
|
## 1. Context
|
||||||
|
|
||||||
The estate has been building multi-tenancy for months and has never written
|
The estate has been building multi-tenancy for months and has never written
|
||||||
down what it is building. Five documents each cover a slice:
|
down what it is building. Five documents each cover a slice:
|
||||||
|
|
@ -32,260 +38,432 @@ down what it is building. Five documents each cover a slice:
|
||||||
| `rapp-postgres/docs/adr/ADR-0001` | Consumer + tenant isolation in PostgreSQL | Proposed, governs one repo |
|
| `rapp-postgres/docs/adr/ADR-0001` | Consumer + tenant isolation in PostgreSQL | Proposed, governs one repo |
|
||||||
| `rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1` | The stacked-boundary gap | Routed 2026-08-10, **still unratified** |
|
| `rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1` | The stacked-boundary gap | Routed 2026-08-10, **still unratified** |
|
||||||
|
|
||||||
The slices do not compose, and three specific failures follow.
|
Four failures follow.
|
||||||
|
|
||||||
**The gap has been diagnosed once and the fix stalled.** The
|
**The gap has been diagnosed once and the fix stalled.** The
|
||||||
shared-platform-relational-storage draft was written precisely to fill this
|
shared-platform-relational-storage draft was written to fill this hole. It has
|
||||||
hole. It has sat unratified for a week and appears in neither canon
|
sat unratified for a week and appears in neither canon directory. §19 attaches
|
||||||
directory. Producing a second orphaned draft would make things worse, not
|
a ratification path so this one does not join it.
|
||||||
better; §13 therefore attaches a ratification path to this one.
|
|
||||||
|
|
||||||
**Placement is owned by nobody.** Whether a workload gets a dedicated
|
**Placement is owned by nobody.** `user-engine-pg` and `target-revenue-pg` are
|
||||||
PostgreSQL cluster or a shared one is currently precedent-by-accident:
|
dedicated; `apps-pg`, `net-kingdom-pg`, `platform-pg`, `state-hub-db` and
|
||||||
`user-engine-pg` and `target-revenue-pg` are dedicated; `apps-pg`,
|
`forgejo-db` are shared. Both patterns are live, neither is written down, and
|
||||||
`net-kingdom-pg`, `platform-pg`, `state-hub-db` and `forgejo-db` are shared.
|
each new service copies whichever neighbour it consulted. `tenant-engine`
|
||||||
Both patterns are live, neither is written down, and each new service copies
|
raised this with `railiance-platform` on 2026-08-16; unanswered.
|
||||||
whichever neighbour it happened to consult. `tenant-engine` raised this with
|
|
||||||
`railiance-platform` on 2026-08-16 and it is unanswered.
|
|
||||||
|
|
||||||
**Two contradictory tenancy defaults are already ratified.**
|
**Two contradictory tenancy defaults are already ratified.**
|
||||||
`business-app-service-contract` §1.2 makes *instance-per-client* the default
|
`business-app-service-contract` §1.2 makes instance-per-client the default for
|
||||||
for business apps. Platform services such as `audit-core` and `tenant-engine`
|
business apps. Platform services pool all tenants into one database. Nothing
|
||||||
pool all tenants into one database. Both are correct for their case, but
|
states which shape a new service takes, and no definition distinguishes the
|
||||||
nothing states which shape a new service takes, and the estate has no
|
two categories.
|
||||||
definition distinguishing "business app" from "platform service". A new
|
|
||||||
repo can read either document and be conformant with one while violating the
|
|
||||||
other.
|
|
||||||
|
|
||||||
## Decision
|
**There is no honest way to describe a repo that is not there yet.** This is
|
||||||
|
the failure draft-1 missed. The estate absorbs and will keep absorbing repos
|
||||||
|
with weak or absent tenant separation — app-local accounts, no tenant column,
|
||||||
|
ad-hoc filtering. Today such a repo is simply non-conformant, which gives it
|
||||||
|
two bad options: misrepresent its posture, or stay outside the framework
|
||||||
|
entirely. Both are worse than a framework that can say "this repo is at level
|
||||||
|
1, its target is level 3, here is the gap and the evidence."
|
||||||
|
|
||||||
### 1. Multi-tenancy is four orthogonal planes, not one property
|
## 2. What this document is
|
||||||
|
|
||||||
The recurring confusion is that "is this multi-tenant?" is treated as a
|
**A framework, not a model.** It does not specify one correct multi-tenancy
|
||||||
single question. It is four, and they are independent:
|
implementation with a fixed set of characteristics. It supplies:
|
||||||
|
|
||||||
| Plane | Question | Owner | State |
|
- **Terminology** — four planes, and graduated levels within each (§3, §4).
|
||||||
|---|---|---|---|
|
- **A declaration** — the posture vector, so any repo can state where it
|
||||||
| **Identity** | How is a tenant named and validated? | `tenant-engine` / IAM Profile | Ratified, solid |
|
actually is (§5).
|
||||||
| **Authorization** | How is a request bound to the tenants it may act for? | `flex-auth` | Contract ratified |
|
- **A conformance rule** — accuracy, not altitude (§6).
|
||||||
| **Data isolation** | Where does the boundary between two tenants' rows sit? | The consuming service | Stated in ADR-0001, unverified fleet-wide |
|
- **Methodology** — analyze, establish, improve, guard (§12).
|
||||||
| **Placement** | Which physical substrate holds a tenant's data? | *unowned* | Precedent only |
|
- **Evidence definitions** — what proves a claimed level (§13).
|
||||||
|
|
||||||
Treating them as one is not a theoretical tidiness problem; it is producing
|
A service is conformant when its declared posture is accurate and its
|
||||||
concrete errors today.
|
trajectory is recorded. A service is non-conformant when it claims a level it
|
||||||
|
cannot evidence — regardless of how high or low that level is.
|
||||||
|
|
||||||
`rapp-postgres`'s `PostgresConsumer` declaration carries a field
|
## 3. Four orthogonal planes
|
||||||
`tenantIsolation: consumer-service-boundary`. That is a **data-isolation**
|
|
||||||
fact recorded in a **placement** artifact. It reads as though the storage
|
|
||||||
platform enforces something. It does not, and ADR-0001 §4 is explicit that it
|
|
||||||
must not pretend to.
|
|
||||||
|
|
||||||
The "dedicated versus shared" argument routinely mixes placement (a capacity
|
"Is this multi-tenant?" is treated as one question. It is four, and they are
|
||||||
and blast-radius decision) with data isolation (a correctness decision).
|
independent:
|
||||||
`tenant-engine` drew the right distinction unprompted on 2026-08-16 —
|
|
||||||
sharing a *cluster* is capacity and cheap to undo; sharing a *database*
|
|
||||||
entangles schemas and is not — and that distinction is adopted here as
|
|
||||||
binding.
|
|
||||||
|
|
||||||
**Decision:** every document, declaration, and plan tier that says
|
| Plane | 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 physical substrate holds a tenant's data? | `railiance-platform` |
|
||||||
|
|
||||||
|
Treating them as one produces concrete errors today. `rapp-postgres`'s
|
||||||
|
`PostgresConsumer` carries a field `tenantIsolation: consumer-service-boundary`
|
||||||
|
— an **E**-plane fact recorded in a **P**-plane artifact, reading as though the
|
||||||
|
storage platform enforces something it does not. The "dedicated versus shared"
|
||||||
|
argument routinely mixes P (capacity, blast radius) with E (correctness).
|
||||||
|
|
||||||
|
The planes are separated **precisely so that each may sit at a different
|
||||||
|
level**. A repo can be strong on identity and weak on enforcement, or the
|
||||||
|
reverse, and the framework must state that without flattening it to a single
|
||||||
|
verdict. That is the whole point, and draft-1 lost it by collapsing E into a
|
||||||
|
single fixed rule.
|
||||||
|
|
||||||
|
**Decision 3.1:** every document, declaration, and plan tier that says
|
||||||
"isolation" MUST name which plane it means.
|
"isolation" MUST name which plane it means.
|
||||||
|
|
||||||
### 2. The identity and authorization planes are settled; this ADR does not reopen them
|
**Decision 3.2:** the planes couple at their top levels — `E4` is only
|
||||||
|
reachable at `P3` or above — and that coupling MUST be stated where it applies
|
||||||
|
rather than used to argue the planes are one.
|
||||||
|
|
||||||
Recorded here only so the model is complete and so no future document
|
## 4. Graduated levels
|
||||||
re-derives them:
|
|
||||||
|
|
||||||
- Tenant identifiers are `tenant:<grouping>:<name>` per ADR-0013, with
|
Each plane carries an ordered ladder. Higher is stronger, not better: the
|
||||||
`tenant:platform` and `tenant:coulomb` reserved and ungrouped. `tenant-engine`
|
right level is the one a service can evidence and its risk warrants.
|
||||||
owns which values exist; the IAM Profile owns the wire format.
|
|
||||||
- Capability roles `PLTF`/`IAM`/`VEN`/`CUS` are non-exclusive and are a
|
|
||||||
separate fact from grouping. `tenant-engine` is the source of truth; the
|
|
||||||
`tenant_roles` token claim is a point-in-time cache that MUST NOT be trusted
|
|
||||||
for privileged, destructive, credential-vending, or `aal2`-class decisions.
|
|
||||||
- `flex-auth` makes authorization decisions; `tenant-engine` is a data source
|
|
||||||
and makes none.
|
|
||||||
- Storage platforms MUST NOT mint, parse, or authorize on tenant identifiers.
|
|
||||||
To them these are opaque strings.
|
|
||||||
|
|
||||||
### 3. Data isolation is enforced by the consuming service, and the platform must never imply otherwise
|
### 4.1 Identity (I) — how the tenant is named
|
||||||
|
|
||||||
This is ADR-0001 §4 promoted to fleet scope.
|
| Level | State |
|
||||||
|
|---|---|
|
||||||
|
| **I0** | No tenant concept. Data is not attributable to a tenant. |
|
||||||
|
| **I1** | A local tenant notion exists but does not conform to `tenant:<grouping>:<name>`. |
|
||||||
|
| **I2** | Canonical identifiers used, validated against the ratified vocabulary; `tenant-engine` is the source of existence. |
|
||||||
|
| **I3** | I2 plus capability roles (`PLTF`/`IAM`/`VEN`/`CUS`) honoured, with live `tenant-engine` re-query for privileged, destructive, credential-vending or `aal2`-class decisions rather than trusting the cached `tenant_roles` claim. |
|
||||||
|
|
||||||
A consumer connects through a pooled role shared across all of its tenants.
|
Note `business-app-service-contract` §2.1 sets app-local accounts as the v1
|
||||||
The database therefore sees one principal, not the requesting tenant, and has
|
baseline for business apps — an explicitly sanctioned low level with recorded
|
||||||
no trustworthy basis on which to filter. Any database-level tenant filter
|
triggers for moving up. That is the pattern this framework generalises.
|
||||||
would depend on the consumer correctly setting a session variable — which is
|
|
||||||
the application-level check the consumer already owes, relocated somewhere it
|
|
||||||
*looks* like a database guarantee without being one.
|
|
||||||
|
|
||||||
- **Mandatory:** consumer-owned data is tenant-keyed from the first
|
### 4.2 Authorization (A) — how a request is bound to a tenant
|
||||||
migration, even where the consumer serves exactly one tenant today.
|
|
||||||
- **Mandatory:** the consumer binds the authenticated identity to the tenants
|
|
||||||
it may act for, at its own service boundary.
|
|
||||||
- **Available, opt-in:** row-level security keyed on a session GUC set at pool
|
|
||||||
checkout, as defence in depth behind a correct service-side check — never
|
|
||||||
as a substitute for one.
|
|
||||||
- **Prohibited:** describing the opt-in layer as a platform guarantee.
|
|
||||||
|
|
||||||
**This is the model's largest residual risk and §9 does not soften it.**
|
| Level | State |
|
||||||
|
|---|---|
|
||||||
|
| **A0** | No authorization, or tenant context not carried on the request. |
|
||||||
|
| **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. |
|
||||||
|
|
||||||
### 4. The isolation ladder
|
### 4.3 Enforcement (E) — where the tenant boundary actually holds
|
||||||
|
|
||||||
Placement decisions are expressed as rungs. Each rung states what it
|
This is the reframed core. Draft-1 asserted one rule for everyone; this is a
|
||||||
actually buys, and — critically — what it does *not* change.
|
ladder whose top is platform-enforced.
|
||||||
|
|
||||||
| Rung | Shape | Data-isolation guarantee | Blast radius of a leaked runtime credential | Live occupants |
|
| Level | Mechanism | Bypassed by |
|
||||||
|---|---|---|---|---|
|
|---|---|---|
|
||||||
| **R0** | Shared tables, tenant column | App-enforced | All tenants of that consumer | Inside every consumer today |
|
| **E0** | None. Data not tenant-keyed; separation is incidental or absent. | Anything. |
|
||||||
| **R1** | Database per consumer, shared cluster | App-enforced *(unchanged)* | All tenants of that consumer | `audit-core`, `tenant-engine` on `platform-pg` |
|
| **E1** | Data tenant-keyed, filtering applied per query at call sites. | One missing predicate. |
|
||||||
| **R2** | Dedicated cluster per consumer | App-enforced *(unchanged)* | All tenants of that consumer | `user-engine-pg`, `target-revenue-pg` |
|
| **E2** | Filtering centralised at a single service-side choke point that binds authenticated identity to permitted tenants. | Code that bypasses the choke point. |
|
||||||
| **R3** | Dedicated cluster per tenant | Structural | One tenant | Business apps per `business-app-service-contract` §1.2 |
|
| **E3** | Platform-assisted: row-level security keyed on a session GUC set at pool checkout, or an equivalent data-access layer the application cannot trivially route around. | A `SECURITY DEFINER` function, a missing policy, or a wrong GUC. |
|
||||||
| **R4** | R3 + separate region/jurisdiction | Structural + residency | One tenant | None |
|
| **E4** | Structural: the connection a workload holds cannot address another tenant's data at all. Requires per-tenant credentials and per-tenant substrate. | Only a platform-level compromise. |
|
||||||
|
|
||||||
The column that matters is the third one. **R0 → R1 → R2 does not improve
|
Three properties of this ladder matter.
|
||||||
tenant isolation at all.** Those rungs buy consumer isolation, capacity
|
|
||||||
predictability, independent backup retention, and a smaller operational blast
|
|
||||||
radius. The tenant boundary stays exactly where §3 puts it: in application
|
|
||||||
code. Only R3 makes it structural.
|
|
||||||
|
|
||||||
**Decision:** R1 is the default for platform services. R3 is the default for
|
**E3 is real and is not forbidden.** Draft-1 rejected RLS on the grounds that
|
||||||
client-facing business apps, as already ratified. A service that is unsure
|
a control depending on the consumer setting a session variable "looks like a
|
||||||
which it is MUST resolve that before choosing a rung (§12.4).
|
database guarantee without being one". The observation is correct; the
|
||||||
|
conclusion was wrong. E3 is materially stronger than E2 — it converts "every
|
||||||
|
call site must remember" into "one checkout path must be right" — and it is
|
||||||
|
the standard mitigation in pool-model systems. What was actually wrong was
|
||||||
|
*calling E3 by E4's name*. The honesty requirement survives; the prohibition
|
||||||
|
does not.
|
||||||
|
|
||||||
### 5. Movement between rungs must be an operational change, not a rebuild
|
**Higher enforcement is the direction of travel, not an optional extra.**
|
||||||
|
Services holding data whose cross-tenant exposure would be a reportable breach
|
||||||
|
SHOULD target E3 or above. The framework's default expectation for a new
|
||||||
|
platform service is **E2 at first serve, E3 recorded as target**.
|
||||||
|
|
||||||
Adopted from `tenant-engine`'s TEN-WP-0009 design, which got this right:
|
**Platform enforcement is a platform obligation, not only a consumer one.**
|
||||||
|
Draft-1 placed the whole burden on consumers permanently. It is now stated
|
||||||
|
that reaching E3 requires the storage platform to *offer* the mechanism —
|
||||||
|
provisioned RLS policies, a documented GUC contract, and a probe.
|
||||||
|
`rapp-postgres` ADR-0001 §4 already commits to provisioning it on request;
|
||||||
|
that offer becomes an obligation with a defined contract (§18.6). Where a
|
||||||
|
consumer wants E3 and the platform has not supplied the mechanism, the gap is
|
||||||
|
the platform's, not the consumer's.
|
||||||
|
|
||||||
- Connect by injected credential only. No cluster, host, namespace, or
|
### 4.4 Placement (P) — which substrate holds the data
|
||||||
database name anywhere in source.
|
|
||||||
|
Draft-1's `R0` ("shared tables, tenant column") is removed: that was an
|
||||||
|
E-plane state mislabelled as placement. Corrected ladder:
|
||||||
|
|
||||||
|
| Level | Shape | Live occupants |
|
||||||
|
|---|---|---|
|
||||||
|
| **P0** | Shares a database with another consumer (schema or tables co-resident). | None sanctioned; the state absorbed repos often arrive in. |
|
||||||
|
| **P1** | Database per consumer, shared cluster. | `audit-core`, `tenant-engine` on `platform-pg` |
|
||||||
|
| **P2** | Dedicated cluster per consumer. | `user-engine-pg`, `target-revenue-pg` |
|
||||||
|
| **P3** | Dedicated cluster per tenant. | Business apps per `business-app-service-contract` §1.2 |
|
||||||
|
| **P4** | P3 plus separate region or jurisdiction. | None |
|
||||||
|
|
||||||
|
**P0 → P1 → P2 does not raise the E level.** Those steps buy consumer
|
||||||
|
isolation, capacity predictability, independent backup retention, and a
|
||||||
|
smaller operational blast radius. They leave the tenant boundary exactly where
|
||||||
|
the E level puts it. Only P3 makes E4 reachable. This is the single most
|
||||||
|
misusable fact in the framework and §11 governs how it may be described
|
||||||
|
commercially.
|
||||||
|
|
||||||
|
**Decision 4.5:** P1 is the default for platform services; P3 is the default
|
||||||
|
for client-facing business apps, as already ratified. A service unsure which
|
||||||
|
it is must resolve that before choosing (§18.4).
|
||||||
|
|
||||||
|
## 5. The posture vector
|
||||||
|
|
||||||
|
A service states its tenancy posture as one level per plane, plus a target and
|
||||||
|
a date:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tenancy:
|
||||||
|
current: { I: 2, A: 3, E: 2, P: 1 }
|
||||||
|
target: { I: 2, A: 3, E: 3, P: 1 }
|
||||||
|
reviewed: "2026-08-17"
|
||||||
|
gap:
|
||||||
|
E: "Choke point exists and is tested; RLS not yet provisioned. Blocked on
|
||||||
|
rapp-postgres offering the GUC contract. Target Q4."
|
||||||
|
```
|
||||||
|
|
||||||
|
Worked examples, as best assessed today and subject to owner correction:
|
||||||
|
|
||||||
|
| Service | Current | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `tenant-engine` | `I2 A3 E2 P1` | Moving to P1 under TEN-WP-0009; E2 via its own service boundary. |
|
||||||
|
| `audit-core` | `I2 A3 E2 P1` | Same shape; holds audit data, so E3 is the more urgent target. |
|
||||||
|
| A newly absorbed repo | `I1 A1 E1 P0` | Conformant **if declared**, with a recorded path. |
|
||||||
|
|
||||||
|
The vector is what makes the framework hold both situations honestly. A repo
|
||||||
|
at `I1 A1 E1 P0` is not a failure — it is a described state with a direction.
|
||||||
|
A repo claiming `E3` with no RLS policy is a failure regardless of how good the
|
||||||
|
rest of it is.
|
||||||
|
|
||||||
|
**Decision 5.1:** the posture vector is declared in the repo, not in the hub,
|
||||||
|
consistent with local-files-are-source-of-truth.
|
||||||
|
|
||||||
|
## 6. Honest absorption — conformance is accuracy, not altitude
|
||||||
|
|
||||||
|
The governing rule of this framework:
|
||||||
|
|
||||||
|
> **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.**
|
||||||
|
|
||||||
|
Consequences, stated deliberately:
|
||||||
|
|
||||||
|
- 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 (§11).
|
||||||
|
- Downgrading is permitted and must be declared. A regression discovered by
|
||||||
|
guarding (§12) is a defect; a regression declared in advance is a decision.
|
||||||
|
|
||||||
|
This is what the plane separation is *for*. Without it, "not rigorous about
|
||||||
|
tenant separation" is a single verdict that a repo either passes or fails.
|
||||||
|
With it, the same repo is `I1 A1 E1 P0` with a path — which is a plan, not an
|
||||||
|
indictment.
|
||||||
|
|
||||||
|
## 7. Portability across placement levels
|
||||||
|
|
||||||
|
Movement between P levels must be an operational change, not a rebuild.
|
||||||
|
Adopted from `tenant-engine`'s TEN-WP-0009 design:
|
||||||
|
|
||||||
|
- Connect by injected credential only — no cluster, host, namespace, or
|
||||||
|
database name in source.
|
||||||
- Own a whole database, never tables inside someone else's. Sharing a cluster
|
- Own a whole database, never tables inside someone else's. Sharing a cluster
|
||||||
is reversible; sharing a database makes relocation a schema merge.
|
is reversible; sharing a database makes relocation a schema merge.
|
||||||
- Idempotent schema creation, so a fresh target comes up correct.
|
- Idempotent schema creation.
|
||||||
- No cross-database joins or co-location assumptions.
|
- No cross-database joins or co-location assumptions.
|
||||||
|
|
||||||
A service meeting these moves R1 → R2 by: create target, dump, restore, swap
|
A service meeting these moves P1 → P2 by: create target, dump, restore, swap
|
||||||
the credential reference, restart. No rebuild, no release, no code review.
|
the credential reference, restart.
|
||||||
|
|
||||||
**Decision:** these four properties are mandatory at every rung, including
|
**Decision 7.1:** mandatory at P1 and above. At P3, SHOULD rather than MUST —
|
||||||
R3. A business app at R3 that hard-codes its database name has forfeited
|
the consolidation `business-app-service-contract` §1.3 preserves is a real
|
||||||
consolidation, which §1.3 of the business-app contract exists to preserve.
|
benefit, but a per-client instance that never moves is not misconformant for
|
||||||
|
naming its own database.
|
||||||
|
|
||||||
### 6. Placement triggers are recorded in advance
|
## 8. Placement triggers
|
||||||
|
|
||||||
A rung change decided under pressure is decided badly. Each service records,
|
A level change decided under pressure is decided badly. Each service records
|
||||||
at provisioning time, what would move it up:
|
at provisioning time what would move it:
|
||||||
|
|
||||||
- Noisy neighbour on a latency-critical path
|
- Noisy neighbour on a latency-critical path
|
||||||
- A compliance or residency requirement
|
- A compliance or residency requirement
|
||||||
- A plan tier that sells isolation (see §7)
|
- A plan tier requiring a higher minimum (§11)
|
||||||
- Backup retention or RTO no longer fitting
|
- Backup retention or RTO no longer fitting
|
||||||
- Connection or memory ceiling reached (see §9)
|
- Connection or memory ceiling reached (§16)
|
||||||
|
|
||||||
**Decision:** placement ownership is `railiance-platform`'s. This ADR
|
**Decision 8.1:** triggers MUST be *monitored*, not merely recorded. A trigger
|
||||||
proposes the ladder and the triggers; it does not appoint per-workload
|
in a YAML comment that nobody re-reads is documentation, not control. The
|
||||||
placement, which remains that repo's call and is the substance of the
|
`platform-pg` ceiling in §16 is the first that requires an actual alert.
|
||||||
2026-08-16 request still open there.
|
|
||||||
|
|
||||||
### 7. Isolation as a product property — the commercial hazard
|
**Decision 8.2:** placement policy ownership is proposed to
|
||||||
|
`railiance-platform`. This framework supplies the ladder, the triggers, and
|
||||||
|
the declaration; it does not appoint per-workload placement. See §18.2 — this
|
||||||
|
remains their call and their open request.
|
||||||
|
|
||||||
Plan tiers that "sell isolation" are anticipated. The hazard is precise and
|
## 9. Credentials as a tenancy control
|
||||||
worth stating before revenue depends on it:
|
|
||||||
|
|
||||||
**Selling a move from R1 to R2 as stronger tenant isolation would be false.**
|
Short-lived leased credentials from OpenBao, re-read at connection checkout,
|
||||||
Per §4 it changes nothing about the tenant boundary. A customer buying
|
with overlap-first rotation, bound the residual risk at every E level below
|
||||||
"isolated" and receiving R2 gets better noisy-neighbour behaviour and an
|
E4: a leaked credential expires rather than persisting. This is stronger than
|
||||||
independent restore path — real value, and not what they think they bought.
|
the industry norm of a long-lived per-service secret and belongs in the
|
||||||
Only R3 changes the answer to "can another tenant's data reach mine".
|
tenancy model, not only in credential-management canon.
|
||||||
|
|
||||||
**Decision:** any plan tier naming isolation MUST cite the rung it delivers
|
**Decision 9.1:** static long-lived database credentials are not a sanctioned
|
||||||
and MUST NOT describe a lower rung in R3's language. `adaptive-pricing` owns
|
path for any service above E0.
|
||||||
plan terms and `tenant-engine` owns which plan a tenant holds; neither
|
|
||||||
currently maps a tier to a rung, and §12.5 asks them to.
|
|
||||||
|
|
||||||
## 8. Alternatives considered
|
## 10. Blast radius must be published
|
||||||
|
|
||||||
**Schema-per-consumer in one database.** Cheaper on connections, allows
|
**Decision 10.1:** every platform holding consumer data MUST publish, in
|
||||||
cross-consumer queries. *Rejected on catalog leakage:* `pg_catalog` is
|
concrete terms, what a leaked runtime credential can and cannot reach at the
|
||||||
readable per-database, so every co-resident can enumerate every other's
|
levels it operates. `rapp-postgres` ADR-0001 §5 is the reference
|
||||||
table and column names regardless of `GRANT`s. Table names alone leak
|
implementation. Where the model cannot provide a guarantee, the platform says
|
||||||
business structure and no privilege setting removes that.
|
so and names the escalation, rather than describing a weaker control in
|
||||||
|
stronger language.
|
||||||
|
|
||||||
**Row-level security as the consumer boundary.** *Rejected:* puts a
|
## 11. Commercial expression — room to be successful
|
||||||
correctness-critical boundary inside query predicates, where one missing
|
|
||||||
policy or a `SECURITY DEFINER` function silently removes it. Retained as
|
|
||||||
opt-in defence in depth for the tenant boundary only (§3).
|
|
||||||
|
|
||||||
**Platform-enforced tenant RLS by default.** *Rejected:* with a pooled
|
Isolation will be sold. The framework must let commercial and presentation
|
||||||
per-consumer role it depends on the consumer setting a session variable
|
work rather than obstruct them, while preventing a claim that cannot be
|
||||||
correctly, producing the appearance of a database guarantee over what is
|
evidenced.
|
||||||
still an application check. A control that misrepresents where it is enforced
|
|
||||||
is worse than an honest absence.
|
|
||||||
|
|
||||||
**Dedicated instance per consumer as the default (R2 everywhere).**
|
**Decision 11.1:** plan tiers are expressed internally as **minimum levels**,
|
||||||
*Rejected as a default* on operational cost — every instance multiplies
|
not as implementation labels. A tier may require `E3 P2`; it need not print
|
||||||
backup, upgrade, and monitoring surface — but it is the honest answer for a
|
that string anywhere customer-facing.
|
||||||
consumer that cannot accept §9's residual risk, and it stays available as an
|
|
||||||
escalation rather than being argued away.
|
|
||||||
|
|
||||||
**Instance-per-tenant everywhere (R3 as the fleet default).** *Rejected:* at
|
**Decision 11.2:** marketing and product language is free. There is no
|
||||||
the estate's tenant grouping taxonomy — which includes `consumer` (private
|
requirement to expose level labels, ladder names, or this document to
|
||||||
individuals) and `family` — R3 per tenant is economically impossible. The
|
customers. "Dedicated infrastructure", "isolated tenancy", "private instance"
|
||||||
taxonomy itself is evidence that pooling is required; you cannot run a
|
all remain available.
|
||||||
cluster per private individual.
|
|
||||||
|
|
||||||
**One tenancy model for the whole estate.** *Rejected:* the ratified
|
**Decision 11.3:** the constraint is on **evidence, not vocabulary**. Any
|
||||||
business-app contract already commits client-facing apps to R3, and platform
|
customer-facing isolation or availability claim must map to a minimum level
|
||||||
services genuinely cannot follow it. The honest answer is two defaults with a
|
the delivering service actually holds, recorded once when the tier is defined.
|
||||||
stated rule for which applies, not a single default that one half violates.
|
The review is internal and happens at tier definition — not per campaign, per
|
||||||
|
page, or per conversation.
|
||||||
|
|
||||||
## 9. Challenge against outside practice
|
**Decision 11.4:** the one hard line — a claim that another tenant *cannot*
|
||||||
|
reach the customer's data requires **E4**. Everything softer than that
|
||||||
|
absolute has room. This is the claim that reaches compliance questionnaires
|
||||||
|
and contracts, and it is the one that must be true.
|
||||||
|
|
||||||
Held against the common industry framing (AWS's silo/pool/bridge model,
|
`adaptive-pricing` owns plan terms; `tenant-engine` owns which plan a tenant
|
||||||
cell-based architectures), the estate lands as follows.
|
holds. Neither currently maps a tier to minimum levels; §18.5 asks them to,
|
||||||
|
for tiers making isolation or availability claims only.
|
||||||
|
|
||||||
**Where the model is stronger than typical.** Most SaaS multi-tenancy
|
## 12. Methodology — analyze, establish, improve, guard
|
||||||
literature models exactly one boundary — tenant-to-tenant. This estate has
|
|
||||||
**two stacked boundaries**: platform-service-to-platform-service (the
|
|
||||||
consumer boundary) and tenant-to-tenant (the princedom boundary). Naming them
|
|
||||||
separately and refusing to enforce both with one mechanism is a genuine
|
|
||||||
strength and is uncommon.
|
|
||||||
|
|
||||||
The refusal to overstate the guarantee is also better than typical. Vendors
|
The framework is operated as a cycle, not consulted as a reference.
|
||||||
routinely describe pool-model RLS as tenant isolation. ADR-0001 §5 states the
|
|
||||||
blast radius in concrete terms instead, which is the practice worth keeping.
|
|
||||||
|
|
||||||
**Where the model is weaker than typical, and this is the finding that
|
**Analyze.** Assess a repo against the four ladders and produce its current
|
||||||
matters.** The pool model's standard mitigation is a *verified* enforcement
|
vector. Output: the `tenancy.current` block with the reasoning recorded.
|
||||||
layer — a shared data-access library, or RLS, that every service is
|
Applies to new services and absorbed ones alike.
|
||||||
demonstrably routed through. This estate has neither. §3 obliges every
|
|
||||||
consumer to enforce the tenant boundary in its own code, and there is
|
|
||||||
currently **no fleet mechanism that verifies any consumer actually does**.
|
|
||||||
|
|
||||||
`audit-core` has AUDIT-WP-0004-T03; `tenant-engine` has its own checks;
|
**Establish.** Declare the target vector and the gap. The target is set by the
|
||||||
`rapp-postgres` runs 15 adversarial probes against the *consumer* boundary
|
data class held, the tenant groupings served, and any plan tier carried — not
|
||||||
and none against the tenant boundary inside a consumer. A single missing
|
by ambition. Output: `tenancy.target` plus a recorded gap per plane.
|
||||||
`WHERE tenant_id = ?` in any consumer is a cross-tenant data breach that no
|
|
||||||
platform control would catch, no probe would fail, and no log would show as
|
**Improve.** Move one plane at a time. Cross-plane moves are where mistakes
|
||||||
an error. This is the highest-severity known gap in the model and §12.3
|
happen — raising P while leaving E untouched is exactly the misstep §4.4
|
||||||
proposes an owner for it.
|
warns about. Each step is ordinary repo work with ordinary evidence.
|
||||||
|
|
||||||
|
**Guard.** Verify continuously that the declared posture still holds. Guarding
|
||||||
|
checks a service **against its own declaration**, not against a universal
|
||||||
|
maximum. This is what makes the verification problem tractable: nobody has to
|
||||||
|
prove every service is at E4; the check is that nobody is below what they
|
||||||
|
declared, and nobody claims what they cannot evidence.
|
||||||
|
|
||||||
|
Regression found by guarding is a defect; regression declared in advance is a
|
||||||
|
decision. The estate has been bitten twice by silent pin rollbacks that
|
||||||
|
produced ordinary-looking 403s and 404s rather than errors. Posture regression
|
||||||
|
will look the same, so guarding must be designed for invisible failure rather
|
||||||
|
than for crashes.
|
||||||
|
|
||||||
|
## 13. Evidence per level
|
||||||
|
|
||||||
|
Guarding requires each level to have a defined evidence artifact. Proposed:
|
||||||
|
|
||||||
|
| Level | Evidence |
|
||||||
|
|---|---|
|
||||||
|
| **I2** | Identifiers validated against the ratified vocabulary; rejection test for a malformed tenant id. |
|
||||||
|
| **I3** | Live re-query demonstrated on an `aal2`-class path; cached-claim path shown not to be used there. |
|
||||||
|
| **A2** | Single choke point identified; test that an unbound request is refused. |
|
||||||
|
| **A3** | Live decision against `flex-auth` with a denial observed at the endpoint, not only at the decision surface. |
|
||||||
|
| **E1** | Every tenant-owned table carries the tenant key. Schema-level check, mechanical. |
|
||||||
|
| **E2** | Choke point identified; adversarial test that an identity bound to tenant A cannot read tenant B. |
|
||||||
|
| **E3** | RLS policies present on every tenant-owned table; probe that a session without the GUC set reads nothing; probe that a wrong GUC reads nothing. |
|
||||||
|
| **E4** | Per-tenant credential demonstrated unable to connect to another tenant's substrate. |
|
||||||
|
| **P1–P4** | Provisioning declaration plus the platform's own isolation probes. |
|
||||||
|
|
||||||
|
**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.2:** the E2 and E3 evidence artifacts do not exist anywhere in
|
||||||
|
the estate today. `rapp-postgres` runs 15 adversarial probes, all against the
|
||||||
|
*consumer* boundary, none against the tenant boundary inside a consumer. This
|
||||||
|
is the framework's largest live gap and §18.3 seeks an owner.
|
||||||
|
|
||||||
|
## 14. Alternatives considered
|
||||||
|
|
||||||
|
**One fixed model with a single set of characteristics** (draft-1). *Rejected
|
||||||
|
by the operator:* it cannot describe a repo that is not there yet, forcing
|
||||||
|
absorbed repos either to misrepresent their posture or to stay outside the
|
||||||
|
framework. A framework that can only describe its own end state is not a
|
||||||
|
framework.
|
||||||
|
|
||||||
|
**A maturity model with a single overall level** (bronze/silver/gold).
|
||||||
|
*Rejected:* collapses the plane separation that makes the framework useful. A
|
||||||
|
service strong on identity and weak on enforcement has a specific, actionable
|
||||||
|
gap; one composite score hides it and invites averaging.
|
||||||
|
|
||||||
|
**Schema-per-consumer in one database** (sanctioning P0). *Rejected:*
|
||||||
|
`pg_catalog` is readable per-database, so every co-resident enumerates every
|
||||||
|
other's table and column names regardless of `GRANT`s. Retained in the ladder
|
||||||
|
as a describable state, never as a sanctioned target.
|
||||||
|
|
||||||
|
**Row-level security as the *consumer* boundary.** *Rejected:* puts a
|
||||||
|
correctness-critical boundary between two services inside query predicates.
|
||||||
|
Retained as E3 for the *tenant* boundary, where the realistic alternative is
|
||||||
|
application code rather than a database.
|
||||||
|
|
||||||
|
**Mandating E4 for everyone.** *Rejected:* the tenant grouping taxonomy
|
||||||
|
includes `consumer` (private individuals) and `family`. A cluster per private
|
||||||
|
individual is economically impossible; the taxonomy is itself evidence that
|
||||||
|
pooling is required.
|
||||||
|
|
||||||
|
**Leaving each repo to its own model.** *Rejected:* this is the status quo,
|
||||||
|
and it produced two contradictory ratified defaults plus an unowned placement
|
||||||
|
question.
|
||||||
|
|
||||||
|
## 15. Challenge against outside practice
|
||||||
|
|
||||||
|
**Where the framework is stronger than typical.** Most SaaS multi-tenancy
|
||||||
|
literature (AWS's silo/pool/bridge framing among them) models exactly 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-plane levels are also stronger than the usual silo/pool/bridge
|
||||||
|
trichotomy, which describes deployment shape only. Silo/pool/bridge is
|
||||||
|
approximately this framework's P plane with the other three missing — which is
|
||||||
|
why it cannot express "pooled infrastructure, structurally enforced
|
||||||
|
boundary", or the reverse.
|
||||||
|
|
||||||
|
**Where it is weaker, and this is the finding that matters.** The pool model's
|
||||||
|
standard mitigation is a *verified* enforcement layer that every service is
|
||||||
|
demonstrably routed through. The estate has the concept (E2/E3) and none of
|
||||||
|
the verification (§13.2). A single missing `WHERE tenant_id = ?` in any
|
||||||
|
consumer is a cross-tenant breach that no platform control catches, no probe
|
||||||
|
fails, and no log shows as an error.
|
||||||
|
|
||||||
**Where the estate has quietly adopted a good pattern without naming it.**
|
**Where the estate has quietly adopted a good pattern without naming it.**
|
||||||
Short-lived leased credentials from OpenBao, re-read at connection checkout,
|
Short-lived leased credentials re-read at checkout, with overlap-first
|
||||||
with overlap-first rotation, is stronger than the industry norm of a
|
rotation, beat the industry norm of a long-lived per-service secret. §9
|
||||||
long-lived per-service secret. It bounds §9's residual risk by credential
|
promotes it to a tenancy control.
|
||||||
lifetime. It deserves to be stated as a tenancy control, not just a
|
|
||||||
credential-management one.
|
|
||||||
|
|
||||||
**Cell-based architecture is the unexplored option.** R2 and R3 partition by
|
**Cell-based architecture remains unexplored.** Neither P nor E describes a
|
||||||
consumer or by tenant. Neither partitions by *cell* — a fixed-size slice of
|
cell — a fixed-size slice of infrastructure serving a bounded set of tenants,
|
||||||
infrastructure serving a bounded set of tenants, sized so that one cell's
|
sized so one cell's failure is survivable and cell count scales linearly.
|
||||||
failure is survivable and cell count scales linearly. Given the estate spans
|
Given the estate spans railiance, NetKingdom, HelixForge and Coulomb, cells
|
||||||
railiance, NetKingdom, HelixForge and Coulomb with tenants layered on top,
|
may fit better than per-tenant clusters at the point P1 runs out (§18.8).
|
||||||
cells may fit better than per-tenant clusters at the point where R1 runs out.
|
|
||||||
Not proposed here; flagged in §12.6 as worth evaluating before the ceiling in
|
|
||||||
§10 is hit rather than after.
|
|
||||||
|
|
||||||
## 10. Scaling demands — the ceiling is closer than it looks
|
## 16. Scaling demands
|
||||||
|
|
||||||
Measured against the live `platform-pg` specification, not estimated.
|
Measured against the live `platform-pg` specification, not estimated.
|
||||||
|
|
||||||
|
|
@ -293,90 +471,84 @@ Measured against the live `platform-pg` specification, not estimated.
|
||||||
instances: 1 (no HA; single-node rail)
|
instances: 1 (no HA; single-node rail)
|
||||||
max_connections: 100
|
max_connections: 100
|
||||||
memory limit: 1Gi
|
memory limit: 1Gi
|
||||||
cpu limit: 1
|
|
||||||
storage: 20Gi
|
|
||||||
per consumer: 14 connections (12 runtime + 2 migration)
|
per consumer: 14 connections (12 runtime + 2 migration)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Connection ceiling: roughly six consumers.** At 14 connections per consumer
|
**Connection ceiling: roughly six consumers.** Seven consumers request 98 of
|
||||||
declaration, seven consumers request 98 of 100 connections, before CNPG's
|
100 before CNPG's instance manager, metrics exporter, and reserved slots. Two
|
||||||
instance manager, the metrics exporter, and superuser-reserved slots. The
|
are provisioned today. **We are at roughly a third of capacity and the third
|
||||||
practical ceiling is about **six**. Two are provisioned today
|
request will not feel like a scaling event.**
|
||||||
(`audit-core`, `tenant-engine`) plus the isolation probe. **We are at
|
|
||||||
roughly a third of capacity, and the third consumer request will not feel
|
|
||||||
like a scaling event.**
|
|
||||||
|
|
||||||
**Memory is likely to bind before connections do.** 100 PostgreSQL backends
|
**Memory likely binds first.** 100 backends against 1Gi is ~10MB per backend
|
||||||
against a 1Gi limit is roughly 10MB per backend for all of shared buffers,
|
for shared buffers, work_mem and overhead. Connection exhaustion errors
|
||||||
work_mem, and per-backend overhead. Connection exhaustion produces a clear
|
clearly; memory pressure OOM-kills and degrades every co-resident at once.
|
||||||
error; memory pressure produces OOM kills and degraded latency across every
|
|
||||||
co-resident consumer at once.
|
|
||||||
|
|
||||||
**Restore time couples all consumers.** Physical backup is instance-wide, so
|
**Raising E is not free on a shared substrate.** RLS with a per-checkout GUC
|
||||||
restoring one consumer means restoring the whole instance to scratch, taking
|
does not itself increase connection count, but it forecloses aggressive
|
||||||
a logical dump, and importing. A consumer's RTO is therefore a function of
|
transaction-level pooling for consumers that adopt it. E3 on P1 interacts with
|
||||||
**total instance size**, not its own. One large consumer silently degrades
|
the ceiling above and should be planned against it.
|
||||||
every co-resident's recovery promise — and per the storage draft §7.3, a
|
|
||||||
consumer must not promise retention or recovery its platform cannot back.
|
|
||||||
|
|
||||||
**`instances: 1` means no tenant on `platform-pg` has HA.** Deliberate on the
|
**Restore time couples all consumers.** Physical backup is instance-wide, so a
|
||||||
current rail and honestly documented, but it means every shared consumer's
|
consumer's RTO is a function of *total* instance size, not its own. One large
|
||||||
availability story is "restart recovery", not high availability. A plan tier
|
consumer silently degrades every co-resident's recovery promise.
|
||||||
promising uptime cannot be satisfied at R1 as currently built.
|
|
||||||
|
|
||||||
**What this implies.** The ladder is not a long-term growth path on the
|
**`instances: 1` means no P1 tenant has HA.** A tier promising uptime cannot
|
||||||
present substrate — it is a two-to-four-consumer runway. The triggers in §6
|
be satisfied at P1 as currently built — an availability floor belongs in §11's
|
||||||
must be monitored, not merely recorded, and connection and memory
|
minimum-level vocabulary alongside isolation.
|
||||||
utilisation belong on the alert list *before* the next consumer is accepted.
|
|
||||||
|
|
||||||
## 11. Consequences
|
## 17. Consequences
|
||||||
|
|
||||||
- The estate gains one vocabulary for a conversation currently held in four
|
- The estate gains one vocabulary and, more importantly, a way to be honest
|
||||||
incompatible ones.
|
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
|
- `tenantIsolation` in `PostgresConsumer` is revealed as a mislabelled field
|
||||||
and should be renamed or moved once this ADR settles (§12.1).
|
(§18.1).
|
||||||
- Two ratified defaults (R1 for platform services, R3 for business apps) are
|
- The two ratified defaults are reconciled by scope, at the cost of needing a
|
||||||
reconciled by scope rather than by one overriding the other, at the cost of
|
classification rule.
|
||||||
needing a rule for classifying a new service.
|
- The verification problem becomes tractable: guard against declaration, not
|
||||||
- The unverified tenant boundary (§9) becomes a named, owned gap rather than
|
against a universal maximum.
|
||||||
an assumption distributed across every repo.
|
- Draft-1's prohibition on RLS is reversed; `rapp-postgres` acquires an
|
||||||
- The `platform-pg` ceiling becomes a planning input rather than a discovery.
|
obligation to define and offer the E3 mechanism.
|
||||||
- Nothing in this ADR changes a running system. It is descriptive of what is
|
- Nothing here changes a running system.
|
||||||
built, plus five decisions that need owner sign-off.
|
|
||||||
|
|
||||||
## 12. Open questions for ratification
|
## 18. Open questions
|
||||||
|
|
||||||
1. **`tenantIsolation` field** — `rapp-postgres`: rename to name its plane
|
1. **`tenantIsolation` field** — `rapp-postgres`: rename to name its plane and
|
||||||
(`tenantBoundaryEnforcement`?), or move it out of the storage declaration
|
carry a level (`tenancy.E: 2`), or move it out of the storage declaration.
|
||||||
entirely.
|
2. **Placement ownership** — `railiance-platform`: accept the ladder, the
|
||||||
2. **Placement ownership** — `railiance-platform`: accept the ladder and
|
triggers, and the monitoring obligation in §8.1, and appoint a recorded
|
||||||
triggers, and appoint a recorded placement owner per workload. This is the
|
placement owner per workload.
|
||||||
2026-08-16 request, still open.
|
3. **E2/E3 evidence artifacts** — *owner needed.* Who builds the adversarial
|
||||||
3. **Tenant-boundary conformance** — *owner needed*. Who builds and runs a
|
cross-tenant probes and the RLS conformance check? Both `audit-core` and
|
||||||
fleet check that a consumer cannot leak tenant A's rows to tenant B?
|
`tenant-engine` have declined fleet-scope work on correct boundary
|
||||||
`audit-core` and `tenant-engine` have both declined fleet-scope work on
|
reasoning, so this needs appointing. Highest-severity gap (§13.2).
|
||||||
correct boundary reasoning, so this needs appointing rather than
|
4. **Business app vs platform service** — Custodian canon: a classification
|
||||||
volunteering. Highest-severity gap in the model.
|
rule, so a new repo knows whether P3 or P1 is its default. Candidate: reuse
|
||||||
4. **Business app vs platform service** — *Custodian canon*: a definition, so
|
`repo-classification-standard_v1.0` rather than invent a second taxonomy.
|
||||||
a new repo knows whether §1.2's R3 default or this ADR's R1 default
|
5. **Tier → minimum level mapping** — `adaptive-pricing` and `tenant-engine`:
|
||||||
applies to it.
|
required only for tiers making isolation or availability claims (§11).
|
||||||
5. **Plan tier → rung mapping** — `adaptive-pricing` and `tenant-engine`:
|
6. **E3 mechanism** — `rapp-postgres`: what exactly is the GUC contract, and
|
||||||
no tier may name isolation without citing a rung (§7).
|
is it provisioned per consumer on request or offered as a standard option?
|
||||||
6. **Cell-based partitioning** — evaluate before the §10 ceiling is reached.
|
7. **Engine neutrality** — the P ladder rests on a PostgreSQL property
|
||||||
7. **Engine neutrality** — whether the ladder should be stated
|
(per-database catalogs). State it engine-specifically and say so, or
|
||||||
engine-neutrally or admit that R1's guarantee rests on a PostgreSQL
|
abstract it and risk a non-Postgres implementation that silently differs?
|
||||||
property (per-database catalogs). Inherited unresolved from the storage
|
Inherited unresolved from the v0.1 draft.
|
||||||
draft.
|
8. **Cell-based partitioning** — evaluate at consumer #4 rather than now?
|
||||||
|
Neither P nor E describes a cell.
|
||||||
|
|
||||||
## 13. Ratification path
|
## 19. Ratification path
|
||||||
|
|
||||||
The predecessor draft stalled because it was routed without one. This ADR:
|
The predecessor draft stalled because it was routed without one.
|
||||||
|
|
||||||
1. Is reviewed by `tenant-engine`, `flex-auth`, `rapp-postgres`,
|
1. Reviewed by `tenant-engine`, `flex-auth`, `rapp-postgres`,
|
||||||
`railiance-platform`, and `adaptive-pricing` against §12.
|
`railiance-platform`, and `adaptive-pricing` against §18.
|
||||||
2. On acceptance, **supersedes** the routing of
|
2. Each of those repos 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 four ladders, the
|
||||||
|
ladders are wrong and this document changes, not the repo.
|
||||||
|
3. On acceptance, **supersedes** the routing of
|
||||||
`rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1-draft.md`,
|
`rapp-postgres/docs/canon-drafts/shared-platform-relational-storage_v0.1-draft.md`,
|
||||||
whose §§3–8 are absorbed above. That draft should then be withdrawn rather
|
whose §§3–8 are absorbed here. That draft is then withdrawn rather than
|
||||||
than left pending, so the estate does not carry two overlapping proposals.
|
left pending.
|
||||||
3. On acceptance, `rapp-postgres/docs/adr/ADR-0001` moves from `proposed` to
|
4. On acceptance, `rapp-postgres/docs/adr/ADR-0001` moves to `accepted` and is
|
||||||
`accepted` and is annotated as the PostgreSQL implementation of this model.
|
annotated as the PostgreSQL implementation of the E and P ladders.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue