Command Center Thoughts Document
This is exactly the kind of real, current evidence the db-design doc's own warning called for — worth working through carefully rather than rushing to a verdict.
FK compatibility — item 5/6
Confirmed: KeystoneUserId (Guid) can replace EntraObjectId (Guid) on both TenantUsers and Technicians with no structural change. Here's the actual reasoning, not just the conclusion:
- Both fields are shape-identical to what they'd become:
Guid,private set, set once inCreate(), never mutated anywhere else in either aggregate (confirmed — no method touchesEntraObjectIdafter construction in either file). A straight rename toKeystoneUserIdchanges nothing about how either aggregate behaves. - The composite indexes stay correct, not just compatible. Both are
(TenantId, EntraObjectId)— scoped per tenant, not globally unique. That's not incidental: it's what allows one real person to legitimately be aTenantUserin more than one RoundTrip customer account (a consultant working two client accounts, say), sinceroundtripapp.onmicrosoft.comis one shared CIAM tenant serving everyone.KeystoneUserIdrepresents the same "one global person" concept, just more centrally — the same reasoning holds, so(TenantId, KeystoneUserId)is still the right shape. - The structural guardrail from
keystone-db-design.md§4 holds up in the real files, not just in theory.TenantUserandTechnicianremain two genuinely separate aggregate roots with two separate tables and two separate composite indexes — nothing shared between them that could let aKeystoneUserIdcross-resolve from one into the other.
One real thing worth flagging, adjacent to the FK question but not blocking it: both aggregates still have their own IsActive, independently toggled, and both doc comments say the same thing — "cannot log in." Once Keystone owns "can this credential authenticate at all" via its own User.IsActive, RoundTrip's local IsActive fields will need a clearly different meaning (e.g., "active in this particular tenant/role" rather than "can authenticate") — otherwise this quietly recreates the exact dual-IsActive ambiguity TRA-434 already found and fixed once, just one layer up. Not a blocker for this compatibility check, but worth someone's attention before the real migration happens.
On your question — genuinely good instinct, and worth answering honestly rather than confidently inventing an answer, since this is actually flagged as an open, unresolved item in the project's own docs, not something already decided.
The real evidence: keystone-framing.md §9 discusses exactly this, under "WAY-49" — it notes waypoint-brd.md's own ADR-009 originally intended for Waypoint to "reuse RoundTrip's own tenant concept for shared suite login, not derive it from Entra." That's real signal that the original design intent was for RoundTrip to be the canonical source of "Tenant" as a concept, not each product inventing its own. But the doc is explicit that this was never actually implemented correctly (Waypoint currently derives TenantId straight from the Entra claim, which is wrong per the ADR), and — critically — it names this as "independent of this decision... it would need fixing whether Waypoint stays on Entra or moves to Keystone." In other words: it's a real, acknowledged gap, explicitly not resolved by anything in the Keystone work.
Your own instinct (Command Center) lines up with the one other piece of real evidence available: Command Center is already described as growing into owning "subscriptions, entitlement, and billing" — and entitlement is inherently tenant-scoped (an account subscribes to products, not an individual person). A tenant/account concept living there, alongside entitlement, would be a coherent fit. But I want to be precise: that's my own reasonable inference from the pieces already in the docs, not something I can point to as an already-decided architectural fact. Worth raising explicitly as a real open question next time this comes up in planning, rather than treating it as settled.
Good shape to start from, and worth engaging with directly — though I want to flag up front that keystone-db-design.md §6 explicitly calls this "a separate service, a separate document, not Keystone's concern at all," so I'll give you real, grounded feedback rather than try to fully design it here.
Your four tables map cleanly onto the simple version of the problem — but the actual documented requirements are more specific than "which products does this account have," and I think there's one real gap worth catching now rather than after building against it.
Where it holds up:
- Tenants — matches the ADR-009 signal from last message (Command Center as the natural home for the canonical tenant concept)
- Products — a straightforward reference table for RoundTrip/Waypoint/Relay
- Subscription — the right place for "this tenant has this product," and probably needs a tier column/reference too, since pricing is per-product-per-tier (Starter/Standard/Professional), not just per-product
The real gap — "Entitlement" as commonly understood doesn't capture what keystone-framing.md §4 actually specifies. Worth quoting the doc directly, since it's precise about this: "entitlement can't just be 'which products does this account have right now.' It has to carry per-account, time-bounded pricing commitments (a rate-card snapshot with an expiry, not an open-ended lock) that apply to future subscriptions too."
That's describing something structurally different from a simple access-grant table. The Founders Program lock isn't "can this tenant reach this product" — it's "what rate does this tenant pay, and until when is that rate guaranteed" — and critically, it has to apply to products the tenant hasn't even subscribed to yet. A plain Entitlement(TenantId, ProductId) table can't represent a commitment about something that doesn't have a row yet. That likely wants its own concept — something like a PriceCommitment or RateLock table, keyed by TenantId alone (not TenantId + ProductId), carrying the locked rate-card reference and an expiry date, checked at the moment a new subscription gets created rather than stored redundantly per-product upfront.
Two smaller things worth folding in while you're there:
- The 90-day notice / no-silent-changes policy implies pricing itself needs versioning with effective dates, not just current prices — otherwise there's no way to represent "the old rate still applies until this date."
- Bundle discounts (10%/20%) are computed, not stored — they key off how many products a tenant currently has, symmetric regardless of order, so this is probably a calculation over live
Subscriptionrows rather than its own table.
This is genuinely good, valuable thinking — but exactly the kind of design that deserves its own document rather than a few rounds of iteration inside a Keystone spike session. Want me to keep going deeper on this now, or would it make more sense as its own dedicated conversation later, given it's explicitly out of scope for what we're building here?