Compare commits

..

No commits in common. "9a6e14e73381c9cf08d9f7ca6739882bcbf79594" and "f8038ecde561b9eb7087cc4ac558e38f7779079d" have entirely different histories.

3 changed files with 273 additions and 626 deletions

View file

@ -82,13 +82,6 @@ nothing prevents number reuse.
**1. A workplan identifier is globally unique.** `PREFIX-WP-NNNN` names exactly **1. A workplan identifier is globally unique.** `PREFIX-WP-NNNN` names exactly
one workplan across the entire fleet, for all time. one workplan across the entire fleet, for all time.
> **Amended 2026-08-17 by `ADR-009` decision 2.** Uniqueness and forward-only
> allocation are **namespace-scoped**, not global; global identity is the pair
> `(namespace, identifier)`, written `PREFIX-WP-NNNN@namespace` when foreign.
> Global sequential allocation would require a central coordinator — the exact
> dependency federation must survive. Everything below holds unchanged **within**
> a namespace, which is where all current work sits.
- A workplan prefix is owned by exactly one repository. No two repositories may - A workplan prefix is owned by exactly one repository. No two repositories may
use the same prefix. use the same prefix.
- A running number is never reused within a prefix, including after a workplan is - A running number is never reused within a prefix, including after a workplan is
@ -103,10 +96,8 @@ It also inverts ADR-001 — a file carrying a hub's private key is the file
holding hub state. holding hub state.
*Target state (C2).* `state_hub_workstream_id` and `state_hub_task_id` become *Target state (C2).* `state_hub_workstream_id` and `state_hub_task_id` become
**deterministic**: UUIDv5 derived from the workplan identifier. Per `ADR-009` **deterministic**: UUIDv5 derived from the globally unique `PREFIX-WP-NNNN`
decision 3 the derivation input is the pair `(namespace, identifier)`, not the identifier. Every instance computes the same value independently, writeback
identifier alone — deriving from the identifier alone would make two forks
holding unrelated work under the same number compute the same UUID. Every instance computes the same value independently, writeback
becomes idempotent, and any number of hub instances may coexist without becomes idempotent, and any number of hub instances may coexist without
coordination. The field shape is unchanged, so consumers keep working; only the coordination. The field shape is unchanged, so consumers keep working; only the
provenance of the value changes. provenance of the value changes.

View file

@ -1,31 +1,25 @@
--- ---
id: ADR-008 id: ADR-008
type: architecture-decision-record type: architecture-decision-record
title: "Multi-Tenancy Framework: Four Planes, Graduated Levels, Declared Posture" title: "Multi-Tenancy Model: Four Planes, One Isolation Ladder"
status: proposed status: proposed
decided_by: Bernd Worsch decided_by: Bernd Worsch
date: "2026-08-17" date: "2026-08-17"
revision: "draft-2" tags: ["architecture", "multi-tenancy", "isolation", "placement", "tenant-engine", "flex-auth", "rapp-postgres", "scaling"]
tags: ["architecture", "multi-tenancy", "isolation", "placement", "maturity", "tenant-engine", "flex-auth", "rapp-postgres", "scaling"]
--- ---
# ADR-008: Multi-Tenancy Framework — Four Planes, Graduated Levels, Declared Posture # ADR-008: Multi-Tenancy Model — Four Planes, One Isolation Ladder
## Status ## Status
**Proposed, draft-2.** Draft-1 (2026-08-17) proposed a single model with fixed **Proposed.** Drafted 2026-08-17 at the operator's request. This document
characteristics. It was rejected on the grounds that the estate does not need decides things currently owned by `tenant-engine`, `flex-auth`,
one model of multi-tenancy — it needs a framework able to describe, hold, and
improve several situations at once, including repos that do not separate
tenants rigorously today and must be developed toward doing so. Draft-2 is
that reframe.
This document decides things currently owned by `tenant-engine`, `flex-auth`,
`rapp-postgres`, `adaptive-pricing`, and `railiance-platform`. It must be `rapp-postgres`, `adaptive-pricing`, and `railiance-platform`. It must be
reviewed by those owners before ratification; §18 lists what each is being reviewed by those owners before ratification; §12 lists what each is being
asked to accept. asked to accept. Until ratified it governs nothing, and the existing
per-repo documents remain authoritative for their own repos.
## 1. Context ## 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:
@ -38,432 +32,260 @@ 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** |
Four failures follow. The slices do not compose, and three specific 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 to fill this hole. It has shared-platform-relational-storage draft was written precisely to fill this
sat unratified for a week and appears in neither canon directory. §19 attaches hole. It has sat unratified for a week and appears in neither canon
a ratification path so this one does not join it. directory. Producing a second orphaned draft would make things worse, not
better; §13 therefore attaches a ratification path to this one.
**Placement is owned by nobody.** `user-engine-pg` and `target-revenue-pg` are **Placement is owned by nobody.** Whether a workload gets a dedicated
dedicated; `apps-pg`, `net-kingdom-pg`, `platform-pg`, `state-hub-db` and PostgreSQL cluster or a shared one is currently precedent-by-accident:
`forgejo-db` are shared. Both patterns are live, neither is written down, and `user-engine-pg` and `target-revenue-pg` are dedicated; `apps-pg`,
each new service copies whichever neighbour it consulted. `tenant-engine` `net-kingdom-pg`, `platform-pg`, `state-hub-db` and `forgejo-db` are shared.
raised this with `railiance-platform` on 2026-08-16; unanswered. Both patterns are live, neither is written down, and each new service copies
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 for `business-app-service-contract` §1.2 makes *instance-per-client* the default
business apps. Platform services pool all tenants into one database. Nothing for business apps. Platform services such as `audit-core` and `tenant-engine`
states which shape a new service takes, and no definition distinguishes the pool all tenants into one database. Both are correct for their case, but
two categories. nothing states which shape a new service takes, and the estate has no
definition distinguishing "business app" from "platform service". A new
repo can read either document and be conformant with one while violating the
other.
**There is no honest way to describe a repo that is not there yet.** This is ## Decision
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."
## 2. What this document is ### 1. Multi-tenancy is four orthogonal planes, not one property
**A framework, not a model.** It does not specify one correct multi-tenancy The recurring confusion is that "is this multi-tenant?" is treated as a
implementation with a fixed set of characteristics. It supplies: single question. It is four, and they are independent:
- **Terminology** — four planes, and graduated levels within each (§3, §4). | Plane | Question | Owner | State |
- **A declaration** — the posture vector, so any repo can state where it |---|---|---|---|
actually is (§5). | **Identity** | How is a tenant named and validated? | `tenant-engine` / IAM Profile | Ratified, solid |
- **A conformance rule** — accuracy, not altitude (§6). | **Authorization** | How is a request bound to the tenants it may act for? | `flex-auth` | Contract ratified |
- **Methodology** — analyze, establish, improve, guard (§12). | **Data isolation** | Where does the boundary between two tenants' rows sit? | The consuming service | Stated in ADR-0001, unverified fleet-wide |
- **Evidence definitions** — what proves a claimed level (§13). | **Placement** | Which physical substrate holds a tenant's data? | *unowned* | Precedent only |
A service is conformant when its declared posture is accurate and its Treating them as one is not a theoretical tidiness problem; it is producing
trajectory is recorded. A service is non-conformant when it claims a level it concrete errors today.
cannot evidence — regardless of how high or low that level is.
## 3. Four orthogonal planes `rapp-postgres`'s `PostgresConsumer` declaration carries a field
`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.
"Is this multi-tenant?" is treated as one question. It is four, and they are The "dedicated versus shared" argument routinely mixes placement (a capacity
independent: and blast-radius decision) with data isolation (a correctness decision).
`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.
| Plane | Question | Vocabulary owner | **Decision:** every document, declaration, and plan tier that says
|---|---|---|
| **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.
**Decision 3.2:** the planes couple at their top levels — `E4` is only ### 2. The identity and authorization planes are settled; this ADR does not reopen them
reachable at `P3` or above — and that coupling MUST be stated where it applies
rather than used to argue the planes are one.
## 4. Graduated levels Recorded here only so the model is complete and so no future document
re-derives them:
Each plane carries an ordered ladder. Higher is stronger, not better: the - Tenant identifiers are `tenant:<grouping>:<name>` per ADR-0013, with
right level is the one a service can evidence and its risk warrants. `tenant:platform` and `tenant:coulomb` reserved and ungrouped. `tenant-engine`
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.
### 4.1 Identity (I) — how the tenant is named ### 3. Data isolation is enforced by the consuming service, and the platform must never imply otherwise
| Level | State | This is ADR-0001 §4 promoted to fleet scope.
|---|---|
| **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. |
Note `business-app-service-contract` §2.1 sets app-local accounts as the v1 A consumer connects through a pooled role shared across all of its tenants.
baseline for business apps — an explicitly sanctioned low level with recorded The database therefore sees one principal, not the requesting tenant, and has
triggers for moving up. That is the pattern this framework generalises. no trustworthy basis on which to filter. Any database-level tenant filter
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.
### 4.2 Authorization (A) — how a request is bound to a tenant - **Mandatory:** consumer-owned data is tenant-keyed from the first
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.
| Level | State | **This is the model's largest residual risk and §9 does not soften it.**
|---|---|
| **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.3 Enforcement (E) — where the tenant boundary actually holds ### 4. The isolation ladder
This is the reframed core. Draft-1 asserted one rule for everyone; this is a Placement decisions are expressed as rungs. Each rung states what it
ladder whose top is platform-enforced. actually buys, and — critically — what it does *not* change.
| Level | Mechanism | Bypassed by | | Rung | Shape | Data-isolation guarantee | Blast radius of a leaked runtime credential | Live occupants |
|---|---|---| |---|---|---|---|---|
| **E0** | None. Data not tenant-keyed; separation is incidental or absent. | Anything. | | **R0** | Shared tables, tenant column | App-enforced | All tenants of that consumer | Inside every consumer today |
| **E1** | Data tenant-keyed, filtering applied per query at call sites. | One missing predicate. | | **R1** | Database per consumer, shared cluster | App-enforced *(unchanged)* | All tenants of that consumer | `audit-core`, `tenant-engine` on `platform-pg` |
| **E2** | Filtering centralised at a single service-side choke point that binds authenticated identity to permitted tenants. | Code that bypasses the choke point. | | **R2** | Dedicated cluster per consumer | App-enforced *(unchanged)* | All tenants of that consumer | `user-engine-pg`, `target-revenue-pg` |
| **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. | | **R3** | Dedicated cluster per tenant | Structural | One tenant | Business apps per `business-app-service-contract` §1.2 |
| **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. | | **R4** | R3 + separate region/jurisdiction | Structural + residency | One tenant | None |
Three properties of this ladder matter. The column that matters is the third one. **R0 → R1 → R2 does not improve
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.
**E3 is real and is not forbidden.** Draft-1 rejected RLS on the grounds that **Decision:** R1 is the default for platform services. R3 is the default for
a control depending on the consumer setting a session variable "looks like a client-facing business apps, as already ratified. A service that is unsure
database guarantee without being one". The observation is correct; the which it is MUST resolve that before choosing a rung (§12.4).
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.
**Higher enforcement is the direction of travel, not an optional extra.** ### 5. Movement between rungs must be an operational change, not a rebuild
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**.
**Platform enforcement is a platform obligation, not only a consumer one.** Adopted from `tenant-engine`'s TEN-WP-0009 design, which got this right:
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.
### 4.4 Placement (P) — which substrate holds the data - Connect by injected credential only. No cluster, host, namespace, or
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. - Idempotent schema creation, so a fresh target comes up correct.
- No cross-database joins or co-location assumptions. - No cross-database joins or co-location assumptions.
A service meeting these moves P1 → P2 by: create target, dump, restore, swap A service meeting these moves R1 → R2 by: create target, dump, restore, swap
the credential reference, restart. the credential reference, restart. No rebuild, no release, no code review.
**Decision 7.1:** mandatory at P1 and above. At P3, SHOULD rather than MUST — **Decision:** these four properties are mandatory at every rung, including
the consolidation `business-app-service-contract` §1.3 preserves is a real R3. A business app at R3 that hard-codes its database name has forfeited
benefit, but a per-client instance that never moves is not misconformant for consolidation, which §1.3 of the business-app contract exists to preserve.
naming its own database.
## 8. Placement triggers ### 6. Placement triggers are recorded in advance
A level change decided under pressure is decided badly. Each service records A rung change decided under pressure is decided badly. Each service records,
at provisioning time what would move it: at provisioning time, what would move it up:
- 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 requiring a higher minimum (§11) - A plan tier that sells isolation (see §7)
- Backup retention or RTO no longer fitting - Backup retention or RTO no longer fitting
- Connection or memory ceiling reached (§16) - Connection or memory ceiling reached (see §9)
**Decision 8.1:** triggers MUST be *monitored*, not merely recorded. A trigger **Decision:** placement ownership is `railiance-platform`'s. This ADR
in a YAML comment that nobody re-reads is documentation, not control. The proposes the ladder and the triggers; it does not appoint per-workload
`platform-pg` ceiling in §16 is the first that requires an actual alert. placement, which remains that repo's call and is the substance of the
2026-08-16 request still open there.
**Decision 8.2:** placement policy ownership is proposed to ### 7. Isolation as a product property — the commercial hazard
`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.
## 9. Credentials as a tenancy control Plan tiers that "sell isolation" are anticipated. The hazard is precise and
worth stating before revenue depends on it:
Short-lived leased credentials from OpenBao, re-read at connection checkout, **Selling a move from R1 to R2 as stronger tenant isolation would be false.**
with overlap-first rotation, bound the residual risk at every E level below Per §4 it changes nothing about the tenant boundary. A customer buying
E4: a leaked credential expires rather than persisting. This is stronger than "isolated" and receiving R2 gets better noisy-neighbour behaviour and an
the industry norm of a long-lived per-service secret and belongs in the independent restore path — real value, and not what they think they bought.
tenancy model, not only in credential-management canon. Only R3 changes the answer to "can another tenant's data reach mine".
**Decision 9.1:** static long-lived database credentials are not a sanctioned **Decision:** any plan tier naming isolation MUST cite the rung it delivers
path for any service above E0. and MUST NOT describe a lower rung in R3's language. `adaptive-pricing` owns
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.
## 10. Blast radius must be published ## 8. Alternatives considered
**Decision 10.1:** every platform holding consumer data MUST publish, in **Schema-per-consumer in one database.** Cheaper on connections, allows
concrete terms, what a leaked runtime credential can and cannot reach at the cross-consumer queries. *Rejected on catalog leakage:* `pg_catalog` is
levels it operates. `rapp-postgres` ADR-0001 §5 is the reference readable per-database, so every co-resident can enumerate every other's
implementation. Where the model cannot provide a guarantee, the platform says table and column names regardless of `GRANT`s. Table names alone leak
so and names the escalation, rather than describing a weaker control in business structure and no privilege setting removes that.
stronger language.
## 11. Commercial expression — room to be successful **Row-level security as the consumer boundary.** *Rejected:* puts a
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).
Isolation will be sold. The framework must let commercial and presentation **Platform-enforced tenant RLS by default.** *Rejected:* with a pooled
work rather than obstruct them, while preventing a claim that cannot be per-consumer role it depends on the consumer setting a session variable
evidenced. correctly, producing the appearance of a database guarantee over what is
still an application check. A control that misrepresents where it is enforced
is worse than an honest absence.
**Decision 11.1:** plan tiers are expressed internally as **minimum levels**, **Dedicated instance per consumer as the default (R2 everywhere).**
not as implementation labels. A tier may require `E3 P2`; it need not print *Rejected as a default* on operational cost — every instance multiplies
that string anywhere customer-facing. backup, upgrade, and monitoring surface — but it is the honest answer for a
consumer that cannot accept §9's residual risk, and it stays available as an
escalation rather than being argued away.
**Decision 11.2:** marketing and product language is free. There is no **Instance-per-tenant everywhere (R3 as the fleet default).** *Rejected:* at
requirement to expose level labels, ladder names, or this document to the estate's tenant grouping taxonomy — which includes `consumer` (private
customers. "Dedicated infrastructure", "isolated tenancy", "private instance" individuals) and `family` — R3 per tenant is economically impossible. The
all remain available. taxonomy itself is evidence that pooling is required; you cannot run a
cluster per private individual.
**Decision 11.3:** the constraint is on **evidence, not vocabulary**. Any **One tenancy model for the whole estate.** *Rejected:* the ratified
customer-facing isolation or availability claim must map to a minimum level business-app contract already commits client-facing apps to R3, and platform
the delivering service actually holds, recorded once when the tier is defined. services genuinely cannot follow it. The honest answer is two defaults with a
The review is internal and happens at tier definition — not per campaign, per stated rule for which applies, not a single default that one half violates.
page, or per conversation.
**Decision 11.4:** the one hard line — a claim that another tenant *cannot* ## 9. Challenge against outside practice
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.
`adaptive-pricing` owns plan terms; `tenant-engine` owns which plan a tenant Held against the common industry framing (AWS's silo/pool/bridge model,
holds. Neither currently maps a tier to minimum levels; §18.5 asks them to, cell-based architectures), the estate lands as follows.
for tiers making isolation or availability claims only.
## 12. Methodology — analyze, establish, improve, guard **Where the model is stronger than typical.** Most SaaS multi-tenancy
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 framework is operated as a cycle, not consulted as a reference. The refusal to overstate the guarantee is also better than typical. Vendors
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.
**Analyze.** Assess a repo against the four ladders and produce its current **Where the model is weaker than typical, and this is the finding that
vector. Output: the `tenancy.current` block with the reasoning recorded. matters.** The pool model's standard mitigation is a *verified* enforcement
Applies to new services and absorbed ones alike. layer — a shared data-access library, or RLS, that every service is
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**.
**Establish.** Declare the target vector and the gap. The target is set by the `audit-core` has AUDIT-WP-0004-T03; `tenant-engine` has its own checks;
data class held, the tenant groupings served, and any plan tier carried — not `rapp-postgres` runs 15 adversarial probes against the *consumer* boundary
by ambition. Output: `tenancy.target` plus a recorded gap per plane. and none against the tenant boundary inside a consumer. A single missing
`WHERE tenant_id = ?` in any consumer is a cross-tenant data breach that no
**Improve.** Move one plane at a time. Cross-plane moves are where mistakes platform control would catch, no probe would fail, and no log would show as
happen — raising P while leaving E untouched is exactly the misstep §4.4 an error. This is the highest-severity known gap in the model and §12.3
warns about. Each step is ordinary repo work with ordinary evidence. proposes an owner for it.
**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 re-read at checkout, with overlap-first Short-lived leased credentials from OpenBao, re-read at connection checkout,
rotation, beat the industry norm of a long-lived per-service secret. §9 with overlap-first rotation, is stronger than the industry norm of a
promotes it to a tenancy control. long-lived per-service secret. It bounds §9's residual risk by credential
lifetime. It deserves to be stated as a tenancy control, not just a
credential-management one.
**Cell-based architecture remains unexplored.** Neither P nor E describes a **Cell-based architecture is the unexplored option.** R2 and R3 partition by
cell — a fixed-size slice of infrastructure serving a bounded set of tenants, consumer or by tenant. Neither partitions by *cell* — a fixed-size slice of
sized so one cell's failure is survivable and cell count scales linearly. infrastructure serving a bounded set of tenants, sized so that one cell's
Given the estate spans railiance, NetKingdom, HelixForge and Coulomb, cells failure is survivable and cell count scales linearly. Given the estate spans
may fit better than per-tenant clusters at the point P1 runs out (§18.8). railiance, NetKingdom, HelixForge and Coulomb with tenants layered on top,
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.
## 16. Scaling demands ## 10. Scaling demands — the ceiling is closer than it looks
Measured against the live `platform-pg` specification, not estimated. Measured against the live `platform-pg` specification, not estimated.
@ -471,84 +293,90 @@ 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.** Seven consumers request 98 of **Connection ceiling: roughly six consumers.** At 14 connections per consumer
100 before CNPG's instance manager, metrics exporter, and reserved slots. Two declaration, seven consumers request 98 of 100 connections, before CNPG's
are provisioned today. **We are at roughly a third of capacity and the third instance manager, the metrics exporter, and superuser-reserved slots. The
request will not feel like a scaling event.** practical ceiling is about **six**. Two are provisioned today
(`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 likely binds first.** 100 backends against 1Gi is ~10MB per backend **Memory is likely to bind before connections do.** 100 PostgreSQL backends
for shared buffers, work_mem and overhead. Connection exhaustion errors against a 1Gi limit is roughly 10MB per backend for all of shared buffers,
clearly; memory pressure OOM-kills and degrades every co-resident at once. work_mem, and per-backend overhead. Connection exhaustion produces a clear
error; memory pressure produces OOM kills and degraded latency across every
co-resident consumer at once.
**Raising E is not free on a shared substrate.** RLS with a per-checkout GUC **Restore time couples all consumers.** Physical backup is instance-wide, so
does not itself increase connection count, but it forecloses aggressive restoring one consumer means restoring the whole instance to scratch, taking
transaction-level pooling for consumers that adopt it. E3 on P1 interacts with a logical dump, and importing. A consumer's RTO is therefore a function of
the ceiling above and should be planned against it. **total instance size**, not its own. One large consumer silently degrades
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.
**Restore time couples all consumers.** Physical backup is instance-wide, so a **`instances: 1` means no tenant on `platform-pg` has HA.** Deliberate on the
consumer's RTO is a function of *total* instance size, not its own. One large current rail and honestly documented, but it means every shared consumer's
consumer silently degrades every co-resident's recovery promise. availability story is "restart recovery", not high availability. A plan tier
promising uptime cannot be satisfied at R1 as currently built.
**`instances: 1` means no P1 tenant has HA.** A tier promising uptime cannot **What this implies.** The ladder is not a long-term growth path on the
be satisfied at P1 as currently built — an availability floor belongs in §11's present substrate — it is a two-to-four-consumer runway. The triggers in §6
minimum-level vocabulary alongside isolation. must be monitored, not merely recorded, and connection and memory
utilisation belong on the alert list *before* the next consumer is accepted.
## 17. Consequences ## 11. Consequences
- The estate gains one vocabulary and, more importantly, a way to be honest - The estate gains one vocabulary for a conversation currently held in four
about partial adoption. incompatible ones.
- 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
(§18.1). and should be renamed or moved once this ADR settles (§12.1).
- The two ratified defaults are reconciled by scope, at the cost of needing a - Two ratified defaults (R1 for platform services, R3 for business apps) are
classification rule. reconciled by scope rather than by one overriding the other, at the cost of
- The verification problem becomes tractable: guard against declaration, not needing a rule for classifying a new service.
against a universal maximum. - The unverified tenant boundary (§9) becomes a named, owned gap rather than
- Draft-1's prohibition on RLS is reversed; `rapp-postgres` acquires an an assumption distributed across every repo.
obligation to define and offer the E3 mechanism. - The `platform-pg` ceiling becomes a planning input rather than a discovery.
- Nothing here changes a running system. - Nothing in this ADR changes a running system. It is descriptive of what is
built, plus five decisions that need owner sign-off.
## 18. Open questions ## 12. Open questions for ratification
1. **`tenantIsolation` field** — `rapp-postgres`: rename to name its plane and 1. **`tenantIsolation` field** — `rapp-postgres`: rename to name its plane
carry a level (`tenancy.E: 2`), or move it out of the storage declaration. (`tenantBoundaryEnforcement`?), or move it out of the storage declaration
2. **Placement ownership** — `railiance-platform`: accept the ladder, the entirely.
triggers, and the monitoring obligation in §8.1, and appoint a recorded 2. **Placement ownership** — `railiance-platform`: accept the ladder and
placement owner per workload. triggers, and appoint a recorded placement owner per workload. This is the
3. **E2/E3 evidence artifacts** — *owner needed.* Who builds the adversarial 2026-08-16 request, still open.
cross-tenant probes and the RLS conformance check? Both `audit-core` and 3. **Tenant-boundary conformance** — *owner needed*. Who builds and runs a
`tenant-engine` have declined fleet-scope work on correct boundary fleet check that a consumer cannot leak tenant A's rows to tenant B?
reasoning, so this needs appointing. Highest-severity gap (§13.2). `audit-core` and `tenant-engine` have both declined fleet-scope work on
4. **Business app vs platform service** — Custodian canon: a classification correct boundary reasoning, so this needs appointing rather than
rule, so a new repo knows whether P3 or P1 is its default. Candidate: reuse volunteering. Highest-severity gap in the model.
`repo-classification-standard_v1.0` rather than invent a second taxonomy. 4. **Business app vs platform service** — *Custodian canon*: a definition, so
5. **Tier → minimum level mapping** — `adaptive-pricing` and `tenant-engine`: a new repo knows whether §1.2's R3 default or this ADR's R1 default
required only for tiers making isolation or availability claims (§11). applies to it.
6. **E3 mechanism** — `rapp-postgres`: what exactly is the GUC contract, and 5. **Plan tier → rung mapping** — `adaptive-pricing` and `tenant-engine`:
is it provisioned per consumer on request or offered as a standard option? no tier may name isolation without citing a rung (§7).
7. **Engine neutrality** — the P ladder rests on a PostgreSQL property 6. **Cell-based partitioning** — evaluate before the §10 ceiling is reached.
(per-database catalogs). State it engine-specifically and say so, or 7. **Engine neutrality** — whether the ladder should be stated
abstract it and risk a non-Postgres implementation that silently differs? engine-neutrally or admit that R1's guarantee rests on a PostgreSQL
Inherited unresolved from the v0.1 draft. property (per-database catalogs). Inherited unresolved from the storage
8. **Cell-based partitioning** — evaluate at consumer #4 rather than now? draft.
Neither P nor E describes a cell.
## 19. Ratification path ## 13. Ratification path
The predecessor draft stalled because it was routed without one. The predecessor draft stalled because it was routed without one. This ADR:
1. Reviewed by `tenant-engine`, `flex-auth`, `rapp-postgres`, 1. Is reviewed by `tenant-engine`, `flex-auth`, `rapp-postgres`,
`railiance-platform`, and `adaptive-pricing` against §18. `railiance-platform`, and `adaptive-pricing` against §12.
2. Each of those repos publishes its own posture vector (§5) as part of 2. On acceptance, **supersedes** the routing 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 here. That draft is then withdrawn rather than whose §§3–8 are absorbed above. That draft should then be withdrawn rather
left pending. than left pending, so the estate does not carry two overlapping proposals.
4. On acceptance, `rapp-postgres/docs/adr/ADR-0001` moves to `accepted` and is 3. On acceptance, `rapp-postgres/docs/adr/ADR-0001` moves from `proposed` to
annotated as the PostgreSQL implementation of the E and P ladders. `accepted` and is annotated as the PostgreSQL implementation of this model.

View file

@ -1,172 +0,0 @@
---
id: ADR-009
type: architecture-decision-record
title: "Federated Namespaces, Qualified Identifiers, and the Limits of Reconciliation"
status: proposed
decided_by: Bernd Worsch
date: "2026-08-17"
tags: ["architecture", "federation", "namespace", "identity", "reconciliation", "fos", "adr-007", "adr-008"]
---
# ADR-009: Federated Namespaces, Qualified Identifiers, and the Limits of Reconciliation
## Status
Proposed. Amends `ADR-007` decision 1 and extends `ADR-008` decision 4.
## Context
The ecosystem must be **federation-capable** — able to work without a live link
to central services — and **chain-of-command efficient**. These read as opposing
requirements, but `federated-organization-standard_v1.0` already resolves the
organizational half:
> global coherence is achieved through policy, escalation, and shared protocols
> rather than through monolithic control (`:51`)
>
> bounded autonomy plus disciplined coordination (`:86`)
Authority is a **policy** layer; availability is a **mechanism**. Nothing
requires the mechanism to depend on reaching the authority — the same separation
distributed version control makes, where an integration branch is privileged by
agreement rather than by any technical property.
**One accepted decision does violate this.** `ADR-007` decision 1 requires
running numbers that are *globally* unique and allocated forward only. Global
sequential allocation requires a coordinator — precisely the central dependency
federation must survive. Two disconnected forks each allocating
`RCLUSTER-WP-0015` is not an edge case but a certainty.
This is not hypothetical. Instance-per-client tenancy (`CUST-WP-0058`) already
means each client deployment is a fork of the stack; the business model committed
to forking before the identifier scheme could express it.
## Decision
**1. Records live in a namespace.** A namespace is a federated branch of the
ecosystem — a fleet instance, a client deployment, an autonomous domain that may
operate disconnected. It is the unit of sovereignty in FOS terms, and the unit of
divergence here.
**2. Identifiers are unique within a namespace, qualified across namespaces.**
`ADR-007` decision 1 is amended: uniqueness and forward-only allocation are
**namespace-scoped**, not global. Global identity is the pair
`(namespace, identifier)`.
Notation is a suffix:
```text
RCLUSTER-WP-0015 this namespace — the common case, unchanged
RCLUSTER-WP-0015@railiance01 a foreign namespace, explicitly qualified
```
Unqualified always means "in my own namespace", exactly as `main` does against
`origin/main`. A suffix keeps existing sorting, grep, and tooling working; a
prefix would break them.
**3. Deterministic identifiers derive from the pair.** `ADR-007` decision 2 (C2)
derives UUIDv5 from `PREFIX-WP-NNNN`. Under federation the derivation input must
be `(namespace, identifier)`. Deriving from the identifier alone would make two
forks holding unrelated work under the same number compute the *same* UUID —
a collision manufactured by the fix. C2 is not yet built, so this is free now and
expensive later.
**4. Records carry lineage.** A namespace records what it forked from and at what
point; a record forked from another namespace records its origin. Without
lineage, a shared identifier is ambiguous between *the same work, diverged* and
*different work, colliding* — a distinction that cannot be reconstructed after
the fact.
**5. Mutable state is derived from an append-only log.** Extending `ADR-008`
decision 4: status and other mutable fields are a fold over append-only events
rather than fields to be reconciled. Reconciliation then becomes union-of-events
plus re-derivation — deterministic, order-independent, and requiring no
conflict model. This collapses the hardest part of reconciliation into the part
that was already free.
**6. Coexistence is the normal case; merging is the exception.** The qualifier is
primarily a **coexistence** mechanism, not merge preparation. Most forks —
client instances above all — are never intended to return. The architecture must
support permanent parallel operation as a first-class outcome, not as a failure
to reconcile.
**7. What may fork.** Canon, plans, knowledge, and records fork freely. **Effects
do not.** Two namespaces may hold divergent plans about one production database;
they cannot hold divergent production databases. Work that acts on shared
operational infrastructure is not fork-safe and must be coordinated through the
owning namespace.
## Reconciliation tiers, and where automation stops
| Tier | Situation | Mechanism | Automatable |
|---|---|---|---|
| **T0 Fork** | namespace created, lineage recorded | metadata | yes |
| **T1 Sync** | pull canon, plans, or knowledge from another namespace | append events, re-derive | largely |
| **T2 Reintegration** | fork returns; records diverged, **no independent operational infrastructure** | union events, re-derive, human-resolve genuine contradictions | partly |
| **T3 Amalgamation** | **distinct operational infrastructure**, identity, credentials, data stores, possibly distinct canon | governed programme — see below | **no** |
**The T2/T3 test is whether the fork established its own operational
infrastructure** — stateful systems with independently diverged state.
**T3 is not a merge.** It resembles the merger or acquisition of distinct
companies far more than a version-control merge, and must be planned as such:
- due diligence — inventory both sides, item by item;
- survivor decisions per subsystem — keep, move, replace, retire;
- migration with evidence, and decommissioning of what does not survive;
- governance integration — reconciling two bodies of canon is a human act,
never automated;
- identity, credential, and data custody consolidation.
**T3 may legitimately end without integration.** Permanent coexistence, or
divestment, are valid outcomes. Real mergers most often fail at systems
integration; an architecture that assumes eventual convergence is lying about
its own limits.
### Known cost
`prj-state-hub-retirement` is a T3 reconciliation inside a *single*
organization — two hub implementations with distinct infrastructure. It required
a dedicated project repository, a 425-item disposition inventory (`SHR-INV-0001`),
staged migration with strangler adapters, and explicit retirement gates. That is
the realistic cost of T3 under the most favourable possible conditions: one
owner, one canon, one credential authority.
Nothing in this ADR reduces that cost. It ensures the cost is **visible before**
a fork is created rather than discovered when reconciliation is attempted.
## Consequences
**Positive.** Disconnected operation stops requiring a coordinator for identity.
`ADR-007` decision 1's contradiction is resolved rather than tolerated. Client
instances become expressible. Reconciliation limits are stated rather than
implied.
**Negative.** Every record gains a namespace and lineage, and tooling must learn
qualified references. The fork-boundary rule (decision 7) constrains what
disconnected work may legitimately do — deliberately.
**Deferred.** Merge machinery for T2 is **not** specified here and should not be
built until a real fork exists and needs it. T3 needs no machinery by definition;
it needs a project repository and human judgement.
**Amended.** `ADR-007` decision 1 — uniqueness is namespace-scoped, not global.
`ADR-007` decision 2 — derivation input becomes `(namespace, identifier)`.
## Open question
**What is the namespace boundary in practice?** Candidates: a fleet instance
(`railiance01`), a client deployment, or an FOS autonomous domain. FOS's
recursion ("from projects to companies to foundation-like umbrella structures")
suggests namespaces nest, which would make qualification relative rather than
absolute. Settle before implementation; the notation works either way.
## References
- `canon/standards/federated-organization-standard_v1.0.md` — bounded autonomy,
escalation, sovereignty by default, rebuildability
- ADR-001 — workplans originate as repo files
- ADR-007 — identifier uniqueness and derived identifiers (amended here)
- ADR-008 — hub authority, local cache, and the two kinds of hub data
- `CUST-WP-0058` — instance-per-client tenancy
- `SHR-INV-0001` — 425-item disposition inventory, T3 cost evidence