Phase 4 Rulings — ADR-078, ADR-079
Phase 4 Rulings — ADR-078, ADR-079
Date: 2026-08-01 · Disposes of:
R4-F-01(🔴),R4-F-02(🔴) fromPhase4_Data_Subject_And_Tenancy_Review.mdApplied directly to the whitepaper (§7.6, §11). NoW7— for the reason R3 gave and this corpus has paid for twice: an intermediate revision document is correct when written and stale three months later. Still open from Phase 4:R4-F-03…R4-F-07.
What the disposition changed about the findings themselves
R4-F-01 proposed crypto-shredding as the direction of fix, on the reading that personal data is spread through mt_events and must be made destroyable in place. Checking that premise before ruling on it showed it was wrong, and the correct ruling is much cheaper.
§9.3’s Entity Segregation & UUIDv7 Linkage already states that when an entity carries long-form text, “the database stores only a globally unique relational ID” and the prose lives in Yjs. §5.1’s domain model is ActorState(ActorId, Position3D, Rotation, Scale) — game state, not personal attributes. ADR-069 keeps events to adjudicated outcomes with inputs as evidence.
The event store is therefore already pseudonymous by construction. What it holds about a person is an opaque identifier; the name behind that identifier lives elsewhere. That property was built for entity-model cleanliness and ID-collision avoidance, and it happens to be the single most valuable privacy property in the architecture — which nothing in the document notices, claims, or protects.
So the ruling is not “add a way to destroy data inside immutable events”. It is: make the existing pseudonymity explicit and enforced, and make the linkage deletable. Encryption survives only as a declared exception for fields that genuinely cannot obey the rule. R4-F-01’s severity was right; its proposed direction was more expensive than the architecture requires.
ADR-078 — Subject Erasure Is Linkage Deletion Over a Pseudonymous Event Store
Status: Accepted · Date: 2026-08-01 · Amends: 008, 036 · Depends-on: 069, 043, 076, 042
Context
The corpus contains no mechanism for removing a named person’s data, and the specification does not use the words erasure, personal data, data subject or PII anywhere (R4-F-01). Meanwhile §7.3 states “Zero Data Deletion” as a principle and §10.4(4) makes never-deleting load-bearing: projection rebuild “stays possible for every room precisely because events are never deleted (ADR-019)”. Read together, those say a deletion request cannot be honoured without breaking something three other decisions rest on.
That reading assumed personal data is inside the events. It is not, or not much: §9.3 already segregates long-form text behind a UUIDv7 link, and §7.3 assigns chat and notes to Yjs rather than to CQRS. The conflict between erasure and immutability is therefore far smaller than it appears — but it is unprotected, because nothing enforces the segregation and nothing makes the linkage deletable.
Decision
1. No domain event, T0 snapshot or T1 payload may carry a personal attribute. A personal attribute is a name, email address, handle, credential, contact detail, or free prose authored by a person. Events carry opaque identifiers only. This is already the design’s behaviour under §9.3; this clause promotes it from an incidental consequence of entity-model hygiene to an enforced invariant, because a property nobody named is a property the next contributor will not preserve.
2. The identity linkage is a Global-scoped record (ADR-042) held outside the event store, mapping SubjectId to the personal attributes. Erasure of a subject is deletion of that record. Every event keeps every byte; seq_id stays contiguous; archival (ADR-036) and shard-scoped rebuild (ADR-043) are untouched. ADR-019’s retained argument about never re-inserting below the projection high-water mark is not weakened, because nothing is re-inserted and nothing is removed. The never-delete invariant and subject erasure are both satisfied, by construction rather than by exception — which is the whole reason this ruling is cheap.
3. A field that genuinely must carry a personal attribute inside an event is a declared exception, stored as ciphertext under a per-subject key held in a keystore outside the event store; erasure destroys the key. The exception list is closed and each entry requires its own ADR, because the cost of an exception is unrecoverable: a field that reaches an event in plaintext before being declared can never be shredded afterwards.
4. Derived artefacts that materialise the linkage are cleared through mechanisms that already exist, not new ones: T2 read models by shard-scoped rebuild (ADR-043), and T0 snapshots by the discard-and-replay path ADR-076 built for schema mismatch. Erasure invalidates affected rooms’ snapshots and lets that path run.
5. Erasure is an offline job with a completion deadline (Q-054), and no backup of the keystore or of the linkage store may be retained beyond it (Q-055). A restore that outlives the deadline resurrects what was erased, which makes the whole mechanism decorative.
6. Explicitly out of scope, and stated so rather than left to inference: Yjs-held content (chat, notes, drawings) — ruled separately by ADR-080 in the same round; and exported archives already delivered to a user — that is R4-F-06 and is unreachable by any platform mechanism.
7. Erasure is durable against undo, fork and timeline checkout, and this is a Depends-on against ADR-042 rather than a mechanism of its own. The linkage record is Global-scoped and Yjs items are TimelineIndependent; neither scope is branch-keyed, so neither participates in ADR-047/ADR-075’s checkout or in §7.3’s undo, and replaying an older branch reconstructs the same game state around an opaque identifier. The property is entirely inherited: it holds only while those two artefacts keep the scopes ADR-042 assigns them, and a future revision of ADR-042 that moved either into Campaign scope would silently make erasure reversible by a GM pressing Undo. That is precisely the failure the Depends-on link type exists to surface at revision time (ADR-077), which is why it is registered rather than trusted.
Alternatives Considered and Why Rejected
- Delete the subject’s events. Breaks
seq_idcontiguity, which ADR-036 preserves deliberately through archival, and invalidates ADR-043’s per-(stream, version)idempotent rebuild. This is the option the finding assumed was necessary; clause 2 exists because it is not. - Rewrite events in place. PostgreSQL implements this as delete-plus-insert — precisely what ADR-019’s argument forbids, and §7.4 rejects the same operation for partition-key mutation for the same reason.
- Encrypt every event field and shred per subject. The direction R4-F-01 proposed. Rejected as over-engineering once the premise was checked: it would encrypt game state that is not personal, forfeit queryability, and add a key-management dependency to the tick path, all to solve a problem §9.3 had already mostly solved.
- Refuse erasure; state the platform cannot do it. Defensible for a hobby project and not for a paid one, and the asymmetry in cost is decisive: keeping the option is nearly free today, and adding it after the first request means re-deriving which of a billion events touched one person.
Consequences (including the negative ones)
- The invariant is only as good as the enforcement list. Clause 1 is enforced by a test over domain event types, which can only reject attribute types it has been told about. A personal attribute smuggled in as an untyped
stringpasses. This is a real hole and the mitigation is a closed vocabulary of personal-attribute types inDescent.Vtt.Domain, not vigilance. - Pseudonymous is not anonymous. After erasure a subject’s events remain linkable to each other through the retained
SubjectId. Whether that is sufficient is a legal question this ADR does not answer and does not pretend to; the architectural claim is that the linkage to a person is destroyed. - Backup retention becomes a correctness constraint, not an operations preference. This is the least architectural part of the ruling and the most likely to be quietly violated.
- Erasure costs a rebuild. Clearing T2 and T0 for a subject’s rooms is real work on the projection worker, bounded by shard scope. It is offline and it competes with nothing on the tick path.
- §7.3’s “Zero Data Deletion” now needs its scope stated, since it is true of game history and not of identity. Left unqualified it reads as a promise this ADR breaks, when in fact the two are about different data.
Enforcement
- ArchitectureTests: no type reachable from a domain event, T0 snapshot or T1 payload may declare a field whose type is in the personal-attribute vocabulary.
- CI: an erasure test over a seeded subject asserts that after erasure the event count and
seq_idsequence are unchanged, a shard-scoped rebuild reproduces byte-identical projections apart from tombstoned identity fields, and no personal attribute is retrievable through any read model. - Runtime metric / SLO:
ErasureJobLatencyandErasureBacklog, alerting when the oldest pending request approachesQ-054. - Release checklist: keystore and linkage-store backup retention is ≤
Q-054, and the declared-exception list (clause 3) has an ADR per entry.
§11 Index Line
ADR-078: Domain events, T0 snapshots and T1 payloads carry opaque identifiers only; the identity linkage is a Global-scoped record outside the event store and subject erasure is deletion of that record, so the never-delete invariant is preserved by construction; personal attributes inside events are declared exceptions stored as per-subject ciphertext; derived artefacts clear through ADR-043 rebuild and ADR-076 discard-and-replay; erasure is durable against undo, fork and checkout because ADR-042 scopes the affected artefacts branch-agnostic.
Section-level directive D-4.1 — §7.3’s permanence claims name their axis
Not an ADR, on ADR-045’s criterion: naming the axis of an existing claim neither contradicts nor narrows a decision, and rejects no alternative a future reader would re-propose. Recorded here because the reasoning is worth keeping and because §7.3’s wording was changed.
The diagnosis is not that §7.3 overstated. Read closely, both claims were already scoped: “the discarded events are never deleted” is scoped to what an Undo does, and “notes recently written by players are never deleted” is scoped by its own opening clause, “Even if combat time travels backwards”. Neither sentence is false.
What was missing is the name of the axis. “Never deleted” is true with respect to the timeline and false with respect to the subject, and one phrase carried both readings. That is this corpus’s recorded failure mode #1 — a claim is only complete with respect to the axis it chose — appearing in a claim rather than in an enumeration, which is where it is harder to see: an incomplete list looks short, whereas an unqualified claim looks confident.
The heading Zero Data Deletion was the specific hazard, because a reader scanning headings takes away a platform-wide property and meets the exception four sections later. §7.3 now names the axis at the point of claim, states that erasure runs in the opposite direction to undo, and §7.6 carries the matching half. §7.3’s note-permanence sentence gains the one qualifier that a promise made to players requires: the permanence is against the timeline, not against the author.
ADR-079 — An ADR That Retains a New Class of Data Must Name Who Can Claim Against It
Status: Accepted · Date: 2026-08-01 · Amends: 045
Context
R4-F-02 is not a defect in any component. §7.2 gives licensed corpora provenance keys, an entitlement filter, and an on-demand revocation purge, and states the rationale exactly: “without provenance keys, a takedown or lapsed licence has no deletion path”. That sentence is equally true of a person, and the design never applied it to one.
Six of Phase 4’s seven findings share that shape — the right mechanism, built for a narrower class than the problem has. §7.1 protects voice and never claims to. ADR-042 makes scope confusion a compile error and leaves entitlement to a runtime predicate. ADR-060 revokes content keys and does not cover exports. ADR-030 reasons about topology and not geography. In every case the mechanism is correct with respect to the concern that motivated it, and the missed class is the one whose claim originates outside the system, where no component misbehaves to draw attention to it.
Phase 3 found seven of ten findings shared a shape and answered it structurally, with the Depends-on link type (ADR-077). This is the same situation and takes the same kind of answer: the failure is not that these questions were answered wrongly, it is that they were never on the form.
Decision
ADR-045’s record format gains one required consideration: when an ADR introduces a new class of retained data, it must name who can make a claim on that data besides the platform, and what honouring such a claim would require. Naming “nobody” is an acceptable answer and is frequently the right one — the requirement is that the question was asked and its answer recorded, not that every ADR grows a compliance section.
“Retained” is the trigger, and it is deliberately narrow: an ADR about a cache, a budget, or a wire format retains nothing and answers in one line. An ADR that causes bytes attributable to a person to persist is the case this exists for.
Alternatives Considered and Why Rejected
- A separate compliance review at release. This is the shape that failed: the angle sat in
Audit_Angles_Tracker.mdas a one-line candidate for months, and that line already contained the whole of R4-F-02. The information was present and unscheduled. Moving the question into the ADR form is the difference between a check someone runs and a check that runs. - A
Claimable-bylink type, parallel toDepends-on. Rejected: link types express relationships between ADRs, and a rights-holder is not an ADR. It would also have suggested the check is mechanical, which it is not — ADR-077’s honest limit applies here with more force, since a missingDepends-onat least has a target that exists. - Extend the enforcement-point rule instead. ADR-045 clause 2 asks what makes this true; this asks who else has a stake. Folding the second into the first would blur a rule that currently works because it is narrow.
Consequences (including the negative ones)
- This has no CI enforcement and cannot have one. It is a question on a form, checked by a human at release. Recorded plainly rather than dressed as a guarantee, exactly as ADR-077 records that a missing
Depends-onhas no symptom. The honest claim is narrow: it converts a question nobody asked into a question with a blank that is visibly empty. - It will produce “nobody” a great many times, and that is the intended steady state. A form field answered “nobody” ninety times and “the player” once has paid for itself.
- Retroactive application is not proposed. Auditing 77 existing ADRs against this would be a Phase 5 angle, not a clause in this one.
Enforcement
- Release checklist: ADRs added or amended in this release each carry the rights-holder consideration, answered.
- ADR record format (§11): the consideration is listed among the mandatory fields.
§11 Index Line
ADR-079: An ADR that introduces a new class of retained data must name who can claim on that data besides the platform and what honouring the claim would require; "nobody" is an acceptable recorded answer. Enforced by release checklist and the §11 record format, with no CI check possible and none claimed.
Phase 4 Rulings, part 2 — ADR-080…083
Date: 2026-08-01 · Disposes of:
R4-F-03,R4-F-04,R4-F-05,R4-F-07.
R4-F-06is closed without an ADR. ADR-078 already wrote the export limit into §7.6, and ADR-045’s criterion makes a decision that neither contradicts an existing ADR nor rejects a re-proposable alternative a section-level directive. Minting an ADR to record something already recorded would inflate the index and teach the next reader that the criterion is decorative. A cross-reference was added to §7.3 so a reader of the export section reaches the limit.
ADR-080 — Yjs Erasure Is Item-Granular and Must Be a CRDT Operation
Status: Accepted · Date: 2026-08-01 · Amends: 026, 042 · Depends-on: 078
Context
ADR-078 covered the event store and explicitly declined Yjs (R4-F-03). Yjs holds the most personal content in the system — chat logs, notebook drawings, player notes — and §7.3 promises exactly that content permanence: “notes recently written by players are never deleted.”
Three properties make this harder than the event store, and all three are stated in §9.3 rather than being speculative. The document is co-authored, so one participant’s erasure changes an artefact five other people rely on. The prose exists twice: as the Yjs blob, and as a flattened plaintext string written by a background job for pg_trgm search. And replicas exist outside the platform, cached in IndexedDB for offline hydration — so any erasure not expressed as a CRDT operation is silently undone by the next reconnecting client.
Decision
1. The erasable unit is the Yjs item, not the document. Every item has exactly one author. Erasability therefore follows authorship granularity, and the co-authorship problem largely dissolves: a shared campaign notebook loses the subject’s items and retains everyone else’s, and a chat stream is a shared document made entirely of sole-authored items.
2. A document all of whose items are authored by the subject is deleted whole, together with its DocId reference from the owning entity (§9.3’s UUIDv7 linkage). A character backstory is the ordinary case.
3. Erasure is expressed as a CRDT delete operation and never as an out-of-band mutation of the stored blob. This is the clause that matters. An offline client holding pre-erasure state re-merges on reconnection under §9.3’s differential synchronisation, so a delete that is not itself a CRDT operation will not converge and the content returns. The delete travels both transports — SFU and, per ADR-026, the SignalR relay — because the relay exists precisely for clients that cannot reach the SFU, and an erasure that skipped it would omit exactly the participants hardest to reach again.
4. The flattened search text is purged in the same job. §9.3 writes the prose a second time as plaintext for pg_trgm. Erasing the blob and leaving the flattened row is a failure this corpus has already found under other names: a derived artefact retaining what its source no longer holds.
5. After the delete converges, the persisted blob is re-encoded so the erased content is not recoverable from it, and this is verified against the Yjs version in use rather than assumed. Yjs garbage-collects deleted content under some configurations and retains it under others; the behaviour is a property of the version and the gc setting. This ADR states the requirement rather than asserting the library’s behaviour, and a CI assertion that the re-encoded blob does not contain the erased bytes is the enforcement — the alternative is trusting a library default across upgrades.
6. Authorship metadata is linkage and erases under ADR-078. A Yjs client identifier is an opaque number once the linkage record is gone.
Alternatives Considered and Why Rejected
- Delete the whole document whenever any author requests erasure. Destroys five other people’s work to satisfy one person’s request, and §7.3’s permanence promise exists for good reasons. Item granularity gives both.
- Rebuild the document from current content, discarding op history. Erases the subject’s items but also every participant’s undo history and attribution, and forces every replica to reset — a heavier operation than a CRDT delete for a worse result.
- Treat all Yjs content as jointly owned and out of scope for erasure. Defensible for a genuinely co-authored notebook and indefensible for a player’s private character notes, which live in the same substrate. The distinction is authorship, which clause 1 already has.
- Purge only the flattened search text and leave the blob. Makes the content unfindable rather than absent. Detection is not protection, and neither is unsearchability.
Consequences (including the negative ones)
- An offline replica retains the content until it reconnects, and the platform cannot bound that window. A client that never reconnects never applies the delete. This is a genuine limit of any CRDT, it is not fixable by design, and stating it is the only honest option.
- A co-author who quoted the erased text authored that quote themselves. Item-granular erasure does not reach it and should not — it is another person’s writing. Worth saying explicitly, because a reader expecting erasure to mean “all trace of this person’s words” will otherwise read it as a defect.
- Erasure now has a convergence dependency, so it is not complete when the job finishes.
Q-054’s deadline is therefore measured to server-side completion; client convergence is best-effort and reported separately. gcbehaviour becomes a version-pinned property of the Yjs upgrade path, adding an item to the dependency-upgrade checklist that did not exist before.
Enforcement
- CI: after erasing a seeded author from a multi-author document, the re-encoded blob does not contain the erased bytes, the flattened search row no longer matches them, and a replica that was offline across the erasure converges to the erased state on reconnection.
- Runtime metric:
YjsErasurePendingConvergence, counting documents whose delete has not been acknowledged by all known replicas. - Release checklist: the Yjs version’s
gcbehaviour was re-verified if the dependency moved.
§11 Index Line
ADR-080: Yjs erasure is item-granular — every item has one author, so a co-authored document loses the subject's items and retains the rest, while a wholly subject-authored document is deleted with its DocId reference. The erasure is a CRDT delete carried on both the SFU and the ADR-026 relay, never an out-of-band blob mutation, or offline replicas re-merge the content; the flattened pg_trgm search text is purged in the same job; the re-encoded blob is CI-asserted not to contain the erased bytes.
ADR-081 — No Audio or Video Is Ever Persisted, as an Invariant Rather Than an Accident
Status: Accepted · Date: 2026-08-01 · Amends: 012 · Amended-by: 108 · interacts with 022
⚠️ Amendment — ADR-108, 2026-08-06
Clause 4 and the second Consequence below are withdrawn. Nothing else in this record is affected, and the text is retained because its reasoning is still the reasoning.
Both scoped this invariant to the platform deployment and excluded the Self-Hosted profile, where the SFU was operator-run and this corpus could say nothing about it. ADR-108 removed that profile (
Architecture_Decision_Rulings_R16.md), so the exclusion has no remaining subject: the invariant is unconditional across every supported deployment.The other limit this record draws is NOT withdrawn and is easy to lose here. ADR-081 never covered participants — a GM or player running local screen or audio capture is outside the guarantee, the platform cannot detect it, and that remains true. One caveat lapsing is not licence to drop the other, and reading this amendment as “the guarantee is now total” would convert an honest scope statement into a false claim.
Context
§9.4 routes individual isolated audio tracks and player webcam streams through the LiveKit SFU, and no part of the specification states a data-handling posture for either. It is nonetheless well protected: §7.1 puts this traffic on WebRTC with “0 DB Writes” and ADR-022 archives are event-derived, so media is structurally incapable of reaching persistent storage.
That protection is real and is documented nowhere as a protection (R4-F-04). It appears only as a consequence of a latency-and-backend-isolation argument. A property that holds by construction but is recorded only as a side effect of a performance argument will not survive an edit made for performance reasons — and the specific edit that would break it is a plausible product request: session recording, implemented by the obvious route of enabling SFU egress.
Decision
1. The platform persists no audio or video, ever — an invariant in its own right rather than something inferred from §7.1’s write-avoidance.
2. The enforcement point is the SFU deployment, not the .NET codebase, because media never enters the .NET process at all. LiveKit’s recording and egress capabilities are not deployed, and their absence is asserted in the infrastructure definition.
3. Session recording, if ever wanted, requires an ADR superseding this one. That is the entire operative effect: it converts a change that currently looks like a configuration flag into a change that has to be argued.
4. The invariant is a property of the platform deployment and does not extend to the Self-Hosted profile, where §10.1 states “LiveKit must be self-run or supplied”. There the SFU is operated by whoever runs the instance and this ADR can say nothing about it. §10.1 treats that as a capability-degradation matter; it is also a data-handling boundary and is now named as one.
Alternatives Considered and Why Rejected
- Leave it implicit; it is already true. This is the finding. Six of Phase 4’s seven findings are properties that were true and unclaimed.
- Assert it with an
ArchitectureTestsrule. No .NET code path could persist media, so the test would pass forever while testing nothing — the shape ADR-064 rejected for blank matrix cells. - Forbid session recording permanently. Overreach: it is a legitimate product feature. The requirement is that adding it is a decision, not a deployment tweak.
Consequences (including the negative ones)
- The guarantee covers the platform’s own deployment, and the self-hosted case is explicitly outside it. Users of a self-hosted instance rely on its operator, which they should be told rather than left to assume the platform’s posture travels with the software.
- Enforcement lives in IaC, a weaker home than CI for a property this important. That is a real limitation, not a presentational one.
Enforcement
- Infrastructure assertion: the deployed LiveKit configuration exposes no egress or recording capability; asserted in IaC and re-checked on deploy.
- Release checklist: the SFU configuration diff is reviewed for recording capability whenever the LiveKit version or configuration changes.
§11 Index Line
ADR-081: No audio or video is persisted by the platform — an invariant in its own right rather than a side effect of §7.1's write-avoidance. Enforced in the SFU deployment because media never enters the .NET process; session recording requires an ADR superseding this one. Covers the platform deployment and explicitly not the Self-Hosted profile, where the SFU is operator-run.
ADR-082 — An Account Is Not an Infrastructure Tenant; Entitlement Becomes Structural
Status: Accepted · Date: 2026-08-01 · Amends: 042, 043
Context
R4-F-05: the word tenant appears nowhere in the specification, all customer data shares one PostgreSQL store, and cross-account isolation is an emergent consequence of every query being correct. ADR-042 makes omitting a scope a compile error but leaves the Licence scope’s entitlement a runtime filter — “filtered by the caller’s entitlement”. Omitting a scope fails to build; omitting a predicate inside a scope is an ordinary bug with no structural defence.
Decision
1. An account is not an infrastructure tenant. One store, one schema, no per-tenant database, no row-level security. Stated plainly, because the ambiguity is worse than either answer: a reader who assumes tenancy exists will not build the guards that actually carry isolation.
2. Row-Level Security is rejected explicitly, so it is not re-proposed as the obvious fix. The projection and lifecycle worker (ADR-043) legitimately reads across every account under a shard lease, so any RLS policy permissive enough for it is permissive enough to defeat the purpose, and any policy strict enough breaks the worker.
3. Cross-account isolation is carried by exactly two named mechanisms, and there is no third. ADR-042’s typed scope surfaces, and an entitlement predicate that is structurally unavoidable: a Licence-scoped repository surface cannot be constructed without an entitlement context, so a query omitting it fails to compile rather than returning another account’s rows. Scope confusion and entitlement omission now fail the same way.
4. §14.7’s disclosure tests gain a cross-account dimension. They currently generate per-viewer filtering properties for visibility inside a room (§8.2), which is a different predicate from across accounts. The generator covers both.
Alternatives Considered and Why Rejected
- Schema- or database-per-account. Defeated by the same worker as RLS, and by ADR-036’s
hash(room_id)partitioning — a room-level scheme that would have to be redesigned around a coarser boundary for no isolation gain the typed surfaces do not already provide. - Leave entitlement a runtime parameter and rely on review. The position §14.4 already rejects for frontend boundaries: “review does not hold for a decade.”
- Declare an account a tenant and defer the mechanism. Would put a word in the document promising a property nothing implements — the exact shape of F-06, where §2.1.1 declared an invariant nothing enforced.
Consequences (including the negative ones)
- The projection worker remains the residual risk and is named as such. It reads across accounts by design, so it is the one component the structural defence cannot cover, and its correctness rests on shard-scoping and review.
- Constructing a
Licencesurface becomes more verbose at every call site — the cost of making omission impossible, and the same trade ADR-042 already accepted. - Nothing here provides data residency per account, which is a different problem this ADR does not solve.
Enforcement
- ArchitectureTests / compile-time: no
Licence-scoped repository surface is constructible without an entitlement context. - CI: §14.7’s generated disclosure tests include cross-account cases; a projection dropping the entitlement predicate fails them.
- Release checklist: new read models declare their scope and, where
Licence-scoped, their entitlement source.
§11 Index Line
ADR-082: An account is not an infrastructure tenant — one store, one schema, and Row-Level Security is rejected because ADR-043's projection worker legitimately reads across accounts. Cross-account isolation is carried by ADR-042's typed scope surfaces plus an entitlement context without which a Licence-scoped surface cannot be constructed, so omitting entitlement fails to compile as scope omission already does; §14.7's disclosure tests gain cross-account cases.
ADR-083 — Primary and DR Regions Are a Declared, Asserted Pair
Status: Accepted · Date: 2026-08-01 · Amends: 030 · Amended-by: 108
⚠️ Amendment — ADR-108, 2026-08-06
The decision and its enforcement point stand unchanged. One rejected alternative lost half of its argument.
“Hardcode a region pair — the platform has a Self-Hosted profile and an operator-chosen deployment; hardcoding is wrong for both” now rests only on the second clause: ADR-108 removed the Self-Hosted profile, so the platform does know its own legal geography.
The declared-and-asserted pair is deliberately retained anyway, and the surviving reason is the stronger one: a crossing pair fails to provision, whereas a hardcoded constant is a value somebody can edit without review. Recorded so a later reader does not read the mechanism as a leftover of a profile that no longer exists and simplify it away.
Context
R4-F-07: ADR-030 fails over from Azure Container Apps to Google Cloud Run, and the specification names no region for either and states no constraint that they be co-located. §10.1.1 is rigorous about DR as a capacity and topology problem; geography is simply not one of its axes. Failover is automated, so an unconstrained region pair relocates live processing of all customer data at the least convenient possible moment, with no point at which anyone reviews it.
Decision
1. The primary and DR regions are a declared pair whose jurisdictional co-location is asserted in the infrastructure definition. The architecture cannot know an operator’s legal geography, so the constraint is not a hardcoded region — it is a declared jurisdiction plus an assertion that both members of the pair sit inside it.
2. A deployment whose pair crosses the declared jurisdiction fails to provision. Not a warning: an automated failover is the wrong moment to discover a deliberate decision was never made.
3. Crossing jurisdictions is permitted where the operator declares it, because for some operators it is the correct trade against availability. It is a configuration with an argument attached, not a default reached by omission.
Alternatives Considered and Why Rejected
- Hardcode a region pair. The platform has a Self-Hosted profile and an operator-chosen deployment; hardcoding is wrong for both.
- Document the constraint in prose without an assertion. ADR-045’s enforcement-point rule applies, and this one has a cheap, natural enforcement point in IaC — so leaving it as prose has no excuse.
- Handle it at failover time with an operator prompt. An incident is when nobody reads prompts.
Consequences (including the negative ones)
- Operators in regions where Cloud Run has no in-jurisdiction presence must either declare the crossing or forgo the DR tier. A real and unpleasant constraint, better encountered at provisioning than during an outage.
- This says nothing about where backups and R2 objects live, which is the same class of question and is not addressed here.
Enforcement
- Infrastructure assertion: provisioning fails when the region pair is outside the declared jurisdiction and the crossing is not explicitly declared.
- Release checklist: the declared jurisdiction and region pair are confirmed whenever either changes.
§11 Index Line
ADR-083: Primary and DR regions are a declared pair whose jurisdictional co-location is asserted in IaC, and a deployment whose pair crosses the declared jurisdiction fails to provision rather than warning. Crossing is permitted where the operator declares it deliberately, because an automated failover is the wrong moment to discover the decision was never made.