IDENTITY-MODEL-PLACEMENT-ANALYSIS-01¶
Brief: IDENTITY-MODEL-PLACEMENT-DECISION-01 (renamed from OPS-DB-SPLIT-SHAPE-DECISION-01 at pre-Gate-1 per F1 sharpening — scope narrowed to identity placement; broader ops-db split decision remains future-queued). Filed: 2026-05-27 by Alex. Status: Phase 1 analysis complete; ready for Tim Phase 2 decision at Gate 1 → Phase 2 boundary. Scope: Substrate audit + three-option analysis. Decision-only; no substrate touch. Output supports Tim's A/B/C decision; downstream IDENTITY-MODEL-MIGRATION-01 executes against the chosen option. Anchors against: ADR-024 (canonical user-identity schema), MANDARIN DATA TAXONOMY, ADR-008 (no platform tier), ADR-019 (Mandarin enforcement at credential surface).
1. Executive summary¶
Three options for canonical identity placement:
- Option A — workspace-db wins. Identity consolidates into
rto-workspace-db. Lowest substrate cost; preserves substrate state direction (workspace-db is already canonical-by-content). MANDARIN-compliant. - Option B — ops-db wins. Identity consolidates into
rto-ops-db. Higher substrate cost — MANDARIN forbids customer-facing workers from reading ops-db, so every workspace-side identity read becomes a service-bound call through internal-api. Architectural purity at code-path expense. - Option C — new identity-db. All ADR-024 tables in
rto-identity-db. Highest substrate cost — new D1 provisioning, new identity service, new CF Access policy, code-path updates across 10+ worker files. Cleaner future federation boundary.
Cross-product with OPS-DB-CONTENT-AUDIT-01's three migration shapes (per F3):
| OPS-DB-CONTENT shape A (4-cluster) | shape B (2-DB) | shape C (3-DB: ops + client-file + access) | |
|---|---|---|---|
| Option A (workspace) | Coherent | Coherent | Conflicts — forces re-work if Shape C wanted later |
| Option B (ops-db) | Coherent | Conflicts — service-routing breaks 2-DB simplicity | Matches — identity becomes "access" cluster |
| Option C (new identity-db) | Bloats to 5-cluster | Bloats to 3-DB (defeats simplest-shape rationale) | Matches — identity-db IS the "access" cluster |
The placement decision partially constrains the broader ops-db split shape. Option A locks Shape C out; Option B locks Shape B out; Option C only fits Shape C.
The analysis below surfaces costs and trade-offs; Tim makes the call.
2. Substrate-state spot-check (shallow per Q4)¶
Verified against IDENTITY-SURFACE-AUDIT-01 (filed hours earlier today):
rto-ops-db identity row counts (post-spot-check): | Table | Rows | Audit match? | |---|---|---| | users | 2 | ✓ | | clients | 1 | ✓ | | operator_roles | 1 | ✓ | | passkey_credentials | 2 | ✓ | | access_allowlist | 6 | ✓ |
rto-workspace-db identity row counts (post-spot-check): | Table | Rows | Audit match? | |---|---|---| | users | 4 | ✓ | | tenants | 1 | ✓ | | user_tenant_roles | 1 | ✓ | | admin_sessions | 1 | ✓ | | impersonation_tokens | 0 | ✓ |
No drift between IMR-01 audit and this analysis. Audit findings stand. Total migration footprint remains < 70 rows across both DBs.
3. The three options — substrate-touching consequences¶
3.1 Option A — Workspace-db wins (substrate-state preservation)¶
Shape. All ADR-024 core tables (users, tier_grants, credentials) plus magic_link_allowlist live in rto-workspace-db. Ops-db copies of users and passkey_credentials migrate out via per-row consolidation. Five orphans drop (OPS-DB-IDENTITY-ORPHAN-CLEANUP-01). tenants retires. Remaining ops-db tables (orgs, partner_accounts, billing_*, etc.) stay in ops-db pending broader split decision.
F4 sharpening — substrate-state direction preservation. Workspace-db already contains operator-tier data (admin_sessions.tier='L3', user_tenant_roles.role='L1' for zero-UUID admin). Option A does NOT introduce operator identity to workspace-db; it consolidates existing operator identity already there. This is the cheapest move because the substrate state is already 80% there.
MANDARIN compliance. workspace-db is Peel category, customer-facing-readable. ✓ Both customer-facing workers and internal-ops workers can read identity directly. No service-binding routing required.
Workers touched (code-path updates):
- apps/workspace/lib/tier-resolution.ts — read tier_grants instead of org+groups synthesis
- apps/workspace/app/auth/verify/route.ts — magic-link verify reads canonical tier_grants
- apps/admin/app/api/admin/me/route.ts — read tier_grants instead of hardcoded tier='L3'
- apps/admin/app/api/admin/sessions/route.ts — read tier_grants for session list
- apps/admin/app/api/admin/administrators/route.ts — magic_link_allowlist (renamed) + tier_grants split
- apps/admin/app/api/admin/users/operators/route.ts — read tier_grants instead of operator_roles
- apps/workspace/lib/session-types.ts — ucca_layer → tier + client_id
Estimated code-path footprint: ~7 worker files. Identity binding stays as WORKSPACE_DB (current; no rewiring).
CF Access: unchanged. workspace-db has no CF Access gate; customer-facing surfaces use magic-link or CF Access via admin domain.
Terraform / D1 provisioning: zero new D1s. No new bindings.
Rollback path: per-row migration with id_migration_map is reversible by construction. Placement reversal (workspace → ops or workspace → new identity-db) is more expensive: requires re-creating tables and re-migrating rows. Cost-of-reversal: ~3-4 hours of substrate work + code-path updates.
3.2 Option B — Ops-db wins (substrate-state reversal)¶
Shape. All ADR-024 core tables in rto-ops-db. Workspace-db copies of users, passkey_credentials, magic_tokens, user_tenant_roles, admin_sessions either migrate to ops-db or retire. tenants retires. impersonation_tokens and portal_invites migrate to ops-db (with tenant_id → client_id reshape). Workspace-db left with customer-facing surface only (no identity tables).
F4 sharpening — substrate-state direction reversal. This inverts the current direction. workspace-db is canonical-by-content today; Option B says "no, ops-db is canonical, workspace-db needs to give up identity." Every workspace identity read becomes a non-trivial change.
MANDARIN compliance — THE LOAD-BEARING CONSTRAINT (per F2). Per standing-rules MANDARIN DATA TAXONOMY:
Customer-facing workers — Cannot read from or write to
rto-ops-db.
This is non-negotiable. Workers affected:
- apps/site (customer-facing) — currently doesn't read identity directly; routes via internal-api proxy. Minor change.
- apps/workspace (customer-facing) — currently reads workspace-db directly for identity. Under Option B, every authenticated request fetches identity via internal-api service binding. Latency + complexity multiplied across every authenticated request.
- workers/prelaunch (customer-facing) — same constraint if it touches identity.
- apps/admin (internal-ops) — can read ops-db directly. Simpler under Option B than Option A (since admin already binds ops-db).
This is the substantive cost of Option B. The brief draft understated it; F2 sharpening surfaces it explicitly.
Workers touched (code-path updates):
- All workers from Option A's list (same tier resolution, session shape, etc.)
- PLUS apps/workspace identity reads converted to service-bound calls
- PLUS new identity endpoints in workers/internal-api to serve customer-facing identity reads
- PLUS observability for the new service boundary (tracing latency, error rates, throttling)
- PLUS apps/workspace caching strategy if direct service hops add unacceptable latency
Estimated code-path footprint: ~10-12 worker files + new internal-api endpoints + caching layer + observability instrumentation.
CF Access: unchanged at the database layer. ops-db has no CF Access on the DB itself; the workers that bind it are CF-Access-gated at the domain layer (admin.rtopacks.com.au) per ADR-019.
Terraform / D1 provisioning: zero new D1s.
Rollback path: per-row reversal is reversible; placement reversal is the same ~3-4 hour substrate work cost. But Option B-to-Option-A reversal also requires undoing the service-binding code paths — adds another 2-3 hours.
3.3 Option C — New identity-db¶
Shape. New D1 rto-identity-db provisioned. All ADR-024 core tables live there. Ops-db and workspace-db both relinquish identity tables. Workers read identity via service-bound calls to a new identity service (likely a new worker, or new responsibility for internal-api).
F6 sharpening — code-path cost is substantial. Per IMR-01 audit, identity reads occur across:
- apps/admin — 4+ routes (me, sessions, administrators, users/operators)
- apps/workspace — auth/verify, lib/tier-resolution.ts, passkey routes
- workers/internal-api — multiple identity-touching routes
- apps/site — via internal-api proxy
Option C routes ALL of these through the new identity service. That's substantially more code-path work than the brief draft listed.
Plus the new substrate: - New D1 provisioning via wrangler - New CF Access policy (or explicit "no CF Access — open within VPC" decision) - New wrangler bindings on every consumer worker (~10+ bindings) - New identity service worker (or new identity endpoints in internal-api) - New service-binding chain from every consumer - New observability surface (cross-service latency, identity-service-down failure modes, etc.)
MANDARIN compliance. Depends on classification:
- If rto-identity-db is classified as customer-facing-readable: customer-facing workers can bind it directly; same MANDARIN posture as Option A.
- If rto-identity-db is classified as internal-only: all reads go through service binding; same MANDARIN posture as Option B (plus the new-DB overhead).
Federation forward-compatibility (per F5 — overstated in original brief). Option C's federation advantage reduces to "slightly cleaner integration boundary for external IDP integrations." ADR-024's credentials.provider opaque column already handles federation at the schema level regardless of DB placement. Federation under Option A or B works fine; under Option C the integration boundary is marginally cleaner.
Workers touched (code-path updates): - All from Option A + all from Option B + new identity service implementation - Service-binding configuration in every consumer worker - Identity service routes (read users, read tier_grants, read credentials)
Estimated code-path footprint: ~12-15 worker files + new identity service worker + service-binding chains.
Terraform / D1 provisioning: one new D1, one new worker (or expanded internal-api), 10+ new bindings.
Rollback path: highest cost. Reversal requires destroying the new D1, undoing all service bindings, re-migrating rows to whatever target replaces it. Cost-of-reversal: ~6-8 hours.
4. Cross-product with OPS-DB-CONTENT-AUDIT-01 shapes (F3)¶
OPS-DB-CONTENT-AUDIT-01 close documented three migration shapes for the broader ops-db split:
- Shape A: 4-cluster (5 DBs): granular per-purpose isolation; 17 binding-adds
- Shape B: 2-DB binary: simplest; 7 binding-adds
- Shape C: 3-DB narrative: ops + client-file + access; 13 binding-adds
Per F3, this brief's three placement options interact with those three split shapes:
4.1 The cross-product matrix¶
| OPS-DB-CONTENT shape A (4-cluster) | OPS-DB-CONTENT shape B (2-DB) | OPS-DB-CONTENT shape C (3-DB ops + client-file + access) | |
|---|---|---|---|
| This brief's Option A (workspace-db) | Coherent — workspace-db absorbs one of the 4 clusters. Adjust "access" cluster framing. | Coherent — workspace-db naturally is the customer-side of the 2-DB binary. Identity sits where customer-facing data sits. | Conflicts — identity-in-workspace-db doesn't match shape C's "access" being its own cluster. Forces re-work if shape C wanted later. |
| This brief's Option B (ops-db) | Coherent — one of the 4 clusters holds identity. | Conflicts — putting identity in ops-db means customer workers can't read direct (MANDARIN); service-routing breaks 2-DB simplicity rationale. | Matches — identity becomes "access" cluster content. Shape C envisaged this. |
| This brief's Option C (new identity-db) | Bloats — adds 5th DB to shape A's 4-cluster. Possible but exceeds shape A's "granular" framing. | Bloats — adds 3rd DB to shape B's 2-DB. Defeats simplest-shape rationale. | Matches — identity-db IS the "access" cluster, explicitly its own DB. Shape C's most direct mapping. |
4.2 Implications¶
Option A composes cleanly with broader Shapes A and B; locks out Shape C. If we pick Option A and later want Shape C (3-DB ops + client-file + access narrative), identity has to migrate again — out of workspace-db into a separate "access" DB.
Option B composes cleanly with Shapes A and C; conflicts with Shape B. If we pick Option B and later want Shape B (2-DB simplest), we're already past simplest because identity routing through internal-api defeats the rationale.
Option C composes naturally with Shape C only. Other shapes bloat.
4.3 The forward-coupling decision¶
The choice has trajectory implications. Tim's eventual broader ops-db split shape decision is being partly made here, by implication:
- Picking Option A signals "we don't expect to want Shape C." We'll be at Shape A or B for the broader split.
- Picking Option B signals "we don't expect to want Shape B simplest." We'll be at Shape A or C.
- Picking Option C signals "we're committing to Shape C narrative — ops / client-file / access as the three top-level DBs."
This is a substantive coupling. The placement-decision-only framing of the brief doesn't capture it; the cross-product surfaces it.
5. Composition with downstream briefs¶
5.1 IDENTITY-MODEL-MIGRATION-01 (the next-brief)¶
Unblocked by this decision. Whichever option Tim picks, the migration brief has its target.
- Option A: migration creates tables in workspace-db; ops-db identity rows migrate out.
- Option B: migration creates tables in ops-db; workspace-db identity rows migrate in; plus extensive service-binding code-path updates in customer-facing workers.
- Option C: migration creates new D1, new service; rows migrate from both ops-db and workspace-db; code-paths across all consumers rewire.
Migration brief effort scales: A < B < C.
5.2 ADMIN-AUTH-MODEL-RECONCILIATION-01¶
Composes with all three options at the credential layer. ADR-024's credentials table is provider-opaque; placement doesn't affect this brief.
5.3 CREDENTIAL-PROVIDER-DECISION-01¶
Composes with all three options. New providers land as new provider values regardless of where credentials lives.
5.4 OPS-DB-IDENTITY-ORPHAN-CLEANUP-01¶
Confirmed standalone per Q5. Drops 5 orphan tables (3 with 1 row each, 2 empty). Composes with whichever migration runs. Compatible with all three options.
5.5 Cross-DB duplicate sub-briefs¶
- CROSS-DB-DUPLICATE-USERS-01 and CROSS-DB-DUPLICATE-PASSKEY-CREDENTIALS-01: subsumed by IMR-01 migration. The placement decision determines which side they consolidate INTO.
- CROSS-DB-DUPLICATE-PRODUCTS-01: out of identity scope; products lives elsewhere. Compatible with all three options.
- CROSS-DB-DUPLICATE-MAGIC-TOKENS-01 (new fourth): drop both copies regardless of option. Compatible with all three.
5.6 OPS-DB-CONTENT-AUDIT-01's broader split¶
Per §4, the placement decision partially constrains the eventual broader split shape. Worth Tim being aware — if the eventual shape preference is C (3-DB narrative), Option B or C of this brief is the consistent choice. If shape preference is A or B, Option A is the cheapest path.
6. Architectural fitness¶
6.1 MANDARIN posture (per F2 reframing)¶
| Option | Customer-facing workers can read identity directly? | Service-binding routing required? |
|---|---|---|
| A — workspace-db | ✓ Yes | No |
| B — ops-db | ✗ No (MANDARIN forbids) | Yes — all workspace identity reads via internal-api |
| C — new identity-db | Depends on classification | Likely required if internal-only |
Option A best honours MANDARIN as currently codified. Option B requires substantial service-binding work to comply with MANDARIN. Option C depends on how the new DB is classified.
6.2 ADR-008 (no platform tier above clients) posture¶
All three options preserve ADR-008 at the commercial layer — clients do not belong under a commercial parent entity. T3 operators per ADR-020 are operational tier, not commercial tier (composition note added to ADR-008 yesterday).
Option A places T3 operator identity in workspace-db. Option B places it in ops-db. Option C places it in dedicated identity-db. None of three creates a commercial parent of clients.
ADR-008 doesn't differentiate between the options.
6.3 ADR-019 (Mandarin enforcement at credential surface)¶
ADR-019 enforces prod-vs-dev separation at the credential surface (DNS + CF Access + admin surface + operator discipline). Placement of identity tables doesn't change this — credentials remain provider-opaque; CF Access remains the DNS gate per environment.
ADR-019 doesn't differentiate between the options.
6.4 Dispositional layer (no-weaponised-lock-in)¶
All three options preserve no-weaponised-lock-in at the architectural layer (ADR-023 deactivate-not-delete; ADR-010 export-portable). Placement choice doesn't create new lock-in vectors.
Spine §1 dispositional layer doesn't differentiate.
6.5 Sentinel posture composition¶
Workspace-db being canonical-by-content (Option A) reads as "substrate continues to be where customer-facing data lives; operator identity is a relatively small fact on the side." Ops-db being canonical (Option B) reads as "substrate makes operators first-class and customer surface depends on operator authority." Both are coherent under the sentinel posture; the framing difference is real but not load-bearing for architectural fitness.
7. Reversibility analysis (per §7 Q3)¶
| Option | Per-row migration reversibility | Placement reversibility |
|---|---|---|
| A | Reversible by construction (id_migration_map) | ~3-4 hours to switch to B or C |
| B | Reversible by construction | ~3-4 hours to switch to A; ~5-6 hours to switch to C (undoing service bindings) |
| C | Reversible by construction | ~6-8 hours to switch to A or B (destroying new D1 + undoing bindings) |
Option A is most reversible. Option C is least reversible. Reversibility is not the primary decision criterion — placement should be picked for its own merits — but worth knowing what we're committing to.
8. Federation forward-compatibility (per §7 Q2, reweighted per F5)¶
Per spine §7: federation is a future credential type. ADR-024's credentials.provider opaque column already handles federation at the schema level regardless of DB placement.
| Option | Federation integration story |
|---|---|
| A | New provider='ucca-federation' row in credentials table. Existing workers read federated identity through the same code paths as native credentials. |
| B | Same. |
| C | Same — slightly cleaner integration boundary because identity is its own service, but the substantive integration mechanism is identical. |
Federation forward-compatibility weighs lightly in this decision. Brief draft overstated Option C's advantage; the actual difference is marginal. ADR-024 already handles federation at the schema level.
9. Cost summary table¶
| Dimension | Option A | Option B | Option C |
|---|---|---|---|
| Substrate work | Lowest | Medium | Highest |
| Code-path updates | ~7 worker files | ~10-12 worker files + internal-api endpoints + caching | ~12-15 worker files + new service worker |
| New D1 provisioning | 0 | 0 | 1 |
| MANDARIN compliance | Native | Requires service-routing | Depends on classification |
| Federation forward-compat | Equal (per ADR-024 schema) | Equal | Marginally cleaner |
| Reversibility | Highest | Medium | Lowest |
| Composes with broader Shape A | ✓ | ✓ | Bloats (5th DB) |
| Composes with broader Shape B | ✓ | ✗ Conflicts | Bloats (3rd DB) |
| Composes with broader Shape C | ✗ Conflicts | ✓ | ✓ Natural fit |
| Strategic narrative fit | "preserve substrate state direction" | "ops-db is authority" | "identity is first-class subsystem" |
10. Open question for Tim at Gate 1 → Phase 2 boundary¶
Which option?
Three readings worth surfacing:
Pragmatic reading: Option A. Lowest substrate cost, fastest path to unblocking IDENTITY-MODEL-MIGRATION-01, preserves substrate state direction. Best for pre-revenue calibration if minimum-viable identity is the goal.
Doctrinal reading: Option B. Honours the MANDARIN intent (operator concerns in ops-db, customer concerns in workspace-db) by placing operator identity where operator concerns live. Costs more in code-path work but architecturally clean.
Forward-positioning reading: Option C. Bets that federation work (spine §7) and the eventual Shape C broader split will both happen; pays the cost now to avoid re-migration later.
My provisional read (not a recommendation, just a frame): Option A is the pre-revenue-appropriate choice. The substrate-state direction already runs toward workspace-db; moving against it for architectural purity (Option B) or future-positioning (Option C) costs work that doesn't have to be paid yet. If Shape C narrative becomes the right broader split shape later, identity can re-migrate then — at a cost similar to picking it now, but only paid if needed.
But I'm not the decision-maker. Tim picks based on which reading the operation prioritises.
11. Open questions for the audit doc itself¶
Three questions Alex should flag for Tim alongside the decision:
(a) ADR-025 file or fold. Per the IMR-01 Q3 pattern. Provisional lean: file — placement is substrate commitment that downstream briefs cite. Final call at Gate 3.
(b) Standing-rules update for decisions/ artefact class. Now created (this is the first artefact filed in it). Worth a small standing-rules sub-rule formalising the four substrate-work artefact classes (audits / designs / recons / decisions). Could land in this brief's Gate 4 commit, or be deferred to the next standing-rules promotion brief (which will also pick up BRIEF-DRAFT-SUBSTRATE-VERIFICATION + SUBSTRATE-BRIEF-GATE-DISCIPLINE candidates). My lean: defer to next standing-rules promotion — the artefact-class formalisation is small enough to compose with existing candidates rather than its own commit.
(c) Whether this analysis should publicly recommend an option. I've written it neutrally with a "my provisional read" subsection. Two alternatives: (i) make a stronger recommendation, (ii) write it without any provisional read. The current shape lets Tim decide without being primed. Stay with current shape unless Tim prefers a stronger recommendation up front.
12. Phase 1 → Phase 2 boundary¶
Audit complete. Phase 2 (Tim's decision) is unblocked.
Phase 2 deliverable: Tim picks A, B, or C with reasoning. The decision goes into ADR-025 (if filed) and into the close report.
Phase 3 (if ADR-025 drafted): Alex drafts the ADR. Single-page; codifies the chosen option as canonical.
End of analysis. Phase 2 decision begins on Tim's signal.