Skip to content

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.tsmagic_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.tsucca_layertier + 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_idclient_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.