Keystone Solution Architecture Document
Version: 1.0
Date: September 2026.
Author: Pete Carroll, Founder — Traxs Group LLC.
Status: Draft.
First real SAD. Describes the architecture the spike proved out, plus the additions needed for the ready-to-use and production tiers (keystone-brd.md §5).
Architecture Overview
Keystone is a .NET service built with the same Clean Architecture RoundTrip and Command Center already use — Core/UseCases/Infrastructure/Web — so any Traxs developer can navigate it immediately. Unlike Command Center at launch, Keystone has real persistent state from day one: it's a credential store, not a pass-through service.
Core architectural principle, proven in the spike: Microsoft's token proves who; Keystone's own data decides what. Entra is one optional federation choice, exactly like "Sign in with Google" — never the required default path every user gets funneled through. This is the entire reason the approach resolves TRA-389: Home Realm Discovery only fires on a deliberately chosen action, never silently during registration.
What Keystone owns: a person's credential (password hash, or a linked external identity via ExternalLogins) and a stable person ID. Nothing else. What Keystone explicitly does not own: entitlement, subscription, billing, or any product-specific user data — all of that stays local to Command Center or to each product respectively.
System Context
┌───────────────────────────────────────────────────────────────────────┐
│ KEYSTONE CONSUMERS │
│ │
│ RoundTrip API/Web Waypoint API/Web Relay (not built) │
│ (TenantUsers, (WaypointUsers, (own user table, │
│ Technicians) own tables) TBD) │
│ │
│ Admin App (not built) — authenticated through Keystone itself │
└──────────┬───────────────────┬──────────────────┬─────────────────────┘
│ OIDC token │ OIDC token │ OIDC token
│ (validated via │ (validated via │ (validated via
│ JWKS, no │ JWKS) │ JWKS)
│ callback needed) │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────────────────────┐
│ KEYSTONE │
│ identity.traxsgroup.com (proposed) │
│ .NET · OpenIddict · FastEndpoints │
│ Own database: credentials, ExternalLogins, │
│ person ID (Section 5) │
└──────────┬───────────────────────────────────────┬─────────────────────┘
│ Optional federation │ Entitlement check
│ ("Sign in with Microsoft", │ (GET /v1/entitlements,
│ never default) │ on token issue/refresh)
▼ ▼
┌─────────────────────┐ ┌─────────────────────────────┐
│ MICROSOFT ENTRA │ │ COMMAND CENTER API │
│ External ID │ │ api.traxsgroup.com │
│ (one app │ │ GET /v1/entitlements — │
│ registration, │ │ live, tested (CC SAD §6.1) │
│ multi-tenant) │ └─────────────────────────────┘
└─────────────────────┘
What's real today (post-spike, pre-hardening): the token-issuance, login, external-federation, and linking mechanics in the left/center of this diagram, proven against a demo project. What's stubbed: the Command Center entitlement call on the right — a hardcoded "yes," not a real HTTP call. What doesn't exist yet: the admin app, at either end.
Solution Structure
Mirrors Command Center's own layout (command-center-sad.md §3), which itself mirrors RoundTrip's — the deliberate point of ADR-007's whole reasoning applied a second time.
KeystoneAPI/
KeystoneAPI.slnx
Directory.Build.props
Directory.Packages.props
src/
Keystone.API.SharedKernel/
EntityBase.cs · IAggregateRoot.cs · ValueObject.cs
IRepository.cs · IReadRepository.cs · IUnitOfWork.cs
Keystone.API.Core/
Aggregates/
IdentityContext/
Account.cs (the credential-owning aggregate root)
ExternalLogin.cs (owned entity — Provider, ProviderKey, Email, LinkedUtc)
Specifications/
AccountByEmailSpec.cs · AccountByExternalLoginSpec.cs
AccountByIdSpec.cs
ValueObjects/
AccountId.cs (the stable person ID other products FK to)
Enums/
AccountStatus.cs
Interfaces/
ICommandCenterEntitlementClient.cs (outbound — PRD §3.11)
IAuditLogger.cs (production tier)
Exceptions/
DomainException.cs
Keystone.API.UseCases/
Result.cs
Common/
Behaviors/
ValidationBehavior.cs · TransactionBehavior.cs
Features/
Registration/
RegisterAccountCommand.cs · RegisterAccountHandler.cs
ExternalLogin/
ChallengeExternalLoginHandler.cs
HandleExternalCallbackCommand.cs · HandleExternalCallbackHandler.cs
ConfirmLinkCommand.cs · ConfirmLinkHandler.cs
UnlinkExternalLoginCommand.cs · UnlinkExternalLoginHandler.cs
PasswordReset/
RequestPasswordResetCommand.cs · RequestPasswordResetHandler.cs
ConfirmPasswordResetCommand.cs · ConfirmPasswordResetHandler.cs
Entitlement/
GetEntitlementClaimsHandler.cs (calls Command Center — Section 6)
Mfa/
EnrollMfaCommand.cs · EnrollMfaHandler.cs
VerifyMfaCommand.cs · VerifyMfaHandler.cs
Admin/
SearchAccountsQuery.cs · ForceResetCommand.cs
MediatorRegistration.cs
Keystone.API.Infrastructure/
Data/
KeystoneDbContext.cs · UnitOfWork.cs · EfRepository.cs
Config/
AccountConfiguration.cs · ExternalLoginConfiguration.cs
Migrations/
Auth/
OpenIddictConfiguration.cs
PasswordHasher.cs
Clients/
CommandCenterEntitlementClient.cs (implements ICommandCenterEntitlementClient)
Services/
EmailService.cs (password-reset emails — likely reuses Command Center's SendGrid pattern)
AuditLogger.cs (production tier)
Keystone.API.Web/
Endpoints/
Account/
RegisterEndpoint.cs
ExternalLoginChallengeEndpoint.cs · ExternalLoginCallbackEndpoint.cs
ConfirmLinkEndpoint.cs · UnlinkExternalLoginEndpoint.cs
PasswordResetRequestEndpoint.cs · PasswordResetConfirmEndpoint.cs
MfaEnrollEndpoint.cs · MfaVerifyEndpoint.cs
Admin/
SearchAccountsEndpoint.cs · ForceResetEndpoint.cs
Program.cs (OpenIddict server registration, `/connect/*` endpoints)
appsettings.json
tests/
Keystone.API.Architecture.Tests/
Keystone.API.UnitTests/
Keystone.API.IntegrationTests/
Fixtures/DatabaseFixture.cs
Keystone.API.FunctionalTests/
Fixtures/KeystoneApiFactory.cs
Deliberate departure from the spike's demo-project structure, proposed here: the spike proved mechanics in a standalone demo; this structure folds those proven patterns into the same five-project Clean Architecture shape Command Center already uses, so the two services read as siblings, not cousins.
Data Model
Proven in spike, carried forward as-is:
Account (aggregate root — the stable person ID)
AccountId Guid PK
Email string, unique
PasswordHash string, nullable (nullable because an external-login-only
account, per Case 3, may never set one)
Status enum (Active/Suspended/Deleted)
CreatedUtc datetime
ExternalLogin (owned entity, one collection per Account)
Id Guid PK
AccountId Guid FK
Provider string ("MicrosoftEntra")
ProviderKey string (Microsoft's stable `oid`)
Email string
LinkedUtc datetime
A real, deliberate departure from the spike's own TenantUser/Technician-shaped proposal, worth naming: the external-login-proposal document built ExternalLogins as a collection directly on TenantUser, with a structurally separate TechnicianExternalLogins table for Technician — correct for a proposal written inside RoundTrip's own codebase, where those are the two real aggregates. Keystone itself only has one kind of account (Account), since RoundTrip's TenantUser/Technician split is a RoundTrip-local concern that references Keystone's AccountId by FK — it is not a distinction Keystone itself needs to represent. The guardrail this was protecting (a technician and a tenant user sharing an email must never cross-resolve) is enforced on RoundTrip's own side, by RoundTrip continuing to look up TenantUsers/Technicians separately by AccountId — Keystone hands back one stable ID; what RoundTrip does with it, and which of its own tables it checks, stays RoundTrip's decision, exactly as it is today.
Proposed, ready-to-use tier:
PasswordResetToken
Token string PK (hashed, not stored plaintext)
AccountId Guid FK
ExpiresUtc datetime
UsedUtc datetime, nullable
Proposed, production tier:
RefreshToken / Session (exact shape depends on OpenIddict's own token-storage
mode — see Section 5)
Id Guid PK
AccountId Guid FK
IssuedUtc datetime
RevokedUtc datetime, nullable (the field Section 3.10's revocation
mechanism actually sets)
MfaSecret
AccountId Guid FK (1:1)
EncryptedSecret string
EnrolledUtc datetime
AuditLogEntry
Id Guid PK
AccountId Guid FK, nullable (nullable — a failed login attempt
against a non-existent email still
gets logged, with no AccountId to
attach it to)
EventType enum (LoginSucceeded/LoginFailed/PasswordChanged/
ExternalLoginLinked/ExternalLoginUnlinked/
AdminForceReset/...)
OccurredUtc datetime
IpAddress string
Detail string, nullable
Explicitly not modeled here, ever: anything from Command Center's schema (Tenant, Subscription, Entitlement) or any product's own user table. AccountId is the only value that crosses the boundary, and only as an opaque FK.
Authentication & Token Design
Proven in spike: OpenIddict as the token issuer, full authorization-code flow, real claims. Token validation is standard JWKS-based — RoundTrip, Waypoint, and Relay validate tokens against Keystone's published signing keys directly, with no callback to Keystone required for a simple "is this token valid" check.
Proposed, resolving PRD §3.11's open design question: entitlement is resolved at token-issuance and refresh time, not on every downstream request. Keystone calls Command Center's GET /v1/entitlements when issuing or refreshing a token, and includes the result as a claim (e.g. entitlements: ["roundtrip", "relay"]) in the token itself. Downstream products read the claim locally — no per-request call to Command Center, no per-request call back to Keystone either. This is the same graceful-degradation-by-caching principle already settled for RoundTrip's own Plan/TrialEndsAt fields (command-center-database.md, resolved 2026-09-03) — Command Center being briefly slow or down doesn't take down every product's every request, it just means a token issued during that window carries stale-until-next-refresh entitlement data, which is an acceptable, bounded staleness window rather than a hard failure.
Real consequence worth being explicit about: access tokens need to be short-lived enough that this staleness window is genuinely small (proposed: 15-60 minutes, refreshed transparently), not so long-lived that a mid-subscription cancellation stays effective for hours. This is a real design trade-off between token lifetime and Command Center call volume — not free in either direction, and worth a deliberate choice rather than picking a default and moving on.
Integration Points
Command Center — Entitlement Delegation (outbound)
ICommandCenterEntitlementClient / CommandCenterEntitlementClient — same pattern as Command Center's own not-yet-built IRoundTripClient (command-center-sad.md §9): HttpClientFactory, a real timeout, graceful degradation on failure. Proposed behavior on failure: if Command Center is unreachable during a token refresh, reuse the previous token's entitlement claims rather than failing the refresh outright — consistent with graceful degradation being non-negotiable platform-wide (command-center-framing.md §4). A brand-new login with no prior token to fall back on, during a Command Center outage, is a real open question this document doesn't resolve — worth flagging rather than quietly picking an answer.
RoundTrip — FK Compatibility
Proven in spike, zero structural changes needed. TenantUsers/Technician reference Keystone's AccountId by FK. The real migration (populating that FK for existing users) is separate, time-sensitive work, gated per keystone-framing.md §8's RoundTrip-specific migration-readiness bar — not part of this architecture, which only needs to prove the shape is compatible.
Waypoint — FK Compatibility
Proven in spike, settled 2026-08-22 as small/mechanical. A KeystoneUserId column replaces WaypointUsers.EntraObjectId as the resolution key; a new WaypointUserExternalLogins table mirrors the same ExternalLogins pattern. Zero live users means zero real data to migrate — a schema decision, not a backfill (keystone-framing.md §9).
Microsoft Entra — Optional Federation
One multi-tenant app registration ("Accounts in any organizational directory"), openid/profile/email scopes only. Proven end-to-end against real Microsoft infrastructure in the spike, including two real claim-naming surprises (oid under its full schema URI, email under a legacy WS-Federation name) that only surfaced by inspecting Entra's actual returned claims — worth remembering as a category of risk (documentation and memory aren't reliable here) rather than a one-time gotcha.
Security Model
Given keystone-brd.md §2.3's centralization-risk framing — Keystone is a single point of failure for the entire platform's identity — this section carries more weight here than the equivalent section would in a single-product SAD.
| Risk | Mitigation | Status |
|---|---|---|
| Credential-stuffing / brute-force against login | Rate limiting on login/reset endpoints (PRD §3.13) | Proposed, production tier |
| A leaked or stolen token/session stays valid | Real revocation mechanism (PRD §3.10) | Proposed, production tier |
| Account takeover via a stolen or guessed password alone | MFA (PRD §3.12) | Proposed, production tier |
| Silent account merging via a spoofed or coincidentally-matching email | Verified-email-required linking, explicit confirm-link step, never automatic | ✅ Proven in spike |
| Forged or tampered tokens | Standard JWT signature validation (OpenIddict); jwt_tool scanning recommended for the production tier (keystone-spike-scope.md §5) | Signature validation proven; adversarial scanning not yet run |
| Undetected compromise or misuse | Real audit logging (PRD §3.14, data model Section 4) | Proposed, production tier |
| Credential-store data loss | Tested backup/recovery (keystone-brd.md §5.3) | Proposed, production tier — explicitly the one dataset where "restore from yesterday" isn't acceptable |
| Broad auth-surface vulnerabilities (injection, misconfiguration, etc.) | OWASP ZAP scanning against login/callback endpoints in CI (keystone-spike-scope.md §5) | Recommended, not yet run |
| Load/availability under real traffic | k6 load testing (keystone-spike-scope.md §5) | Recommended, not yet run — matters more here than for Command Center, since an identity outage takes every product down at once |
Testing Strategy
Proven approach, from the spike: 69 automated tests, structured around real evidence rather than manual click-through — every handler, every linking case, every anti-enumeration guarantee, and (where realistically automatable) every HTTP endpoint.
Proposed for the real build, adopting Command Center's own three-layer pattern (command-center-sad.md §8), since it's already proven valuable there:
- Unit tests — mocked repositories, no database. Where judgment calls (e.g. token-claim entitlement caching behavior on Command Center failure, Section 6.1) get pinned down explicitly.
- Integration tests — a real, shared SQL Server container, catching what unit tests can't: whether
ExternalLoginreally loads as an owned entity with no explicitInclude(), whether theAccount.Emailunique constraint is actually enforced. - Functional tests — a real running instance via
WebApplicationFactory, making real HTTP requests through the actual OpenIddict pipeline — the only layer that proves token issuance, the external-federation redirect chain, and the confirm-link flow work together end to end, not just in isolation.
Additional tooling specific to identity, per keystone-spike-scope.md §5 (already researched, free/open-source): OAuth Tools and OIDC Debugger for manual protocol-level verification during development; jwt_tool for adversarial token testing; OWASP ZAP for broader auth-surface scanning; k6 for load testing. None of these replace the automated suite above — they cover classes of risk (adversarial tampering, load) that a normal test suite doesn't reach.
Deployment / Infrastructure
Proposed — Keystone doesn't exist as a deployed service yet.
| Resource | Proposed Name |
|---|---|
| Resource Group | rg-keystone-production |
| App Service | app-keystone-production |
| Key Vault | kv-keystone-pro |
| Custom Domain | identity.traxsgroup.com |
| Database | Azure SQL, own instance — never shared with any product or with Command Center |
Key Vault secrets (proposed):
| Secret | Purpose |
|---|---|
Entra--ClientId / Entra--ClientSecret | Optional federation app registration |
ConnectionStrings--DefaultConnection | Credential store |
CommandCenter--ApiKey | Outbound entitlement calls |
SendGrid--ApiKey | Password-reset emails (likely reuses Command Center's existing SendGrid integration rather than a second one — worth confirming rather than assuming a duplicate integration is wanted) |
Same region as RoundTrip and Command Center (Central US), matching platform convention.
Rollout Sequencing
Not a new decision — restating what keystone-framing.md §8 already settled, since a SAD is where an implementer would look for it:
- Waypoint and Relay adopt Keystone first. Both are pre-launch or in-development — real production mileage under real (if lower-stakes) traffic, with no paying customer at risk if something's wrong.
- RoundTrip migrates last, once both the bake-in period above and RoundTrip's own specific migration-readiness gate are satisfied: a tested account-recovery flow, a real rollback path, and the "set up your new login" user communication designed and ready (
keystone-brd.md§5.4).
This sequencing is why Sections 5.3's production-tier items (MFA, revocation, audit logging, admin app) can be planned now without needing to exist before Waypoint's first real login — they harden the service Waypoint and Relay are already living on, rather than gating whether that adoption can start.
Growth Path
Current (spike, complete):
OpenIddict token issuance, full interactive auth-code flow
Email/password registration and login
Optional Entra federation, all three ExternalLogins linking cases
FK-compatibility confirmed against RoundTrip and Waypoint
Ready-to-use tier (needed now):
Real password reset (request + confirm)
Real Command Center entitlement call (replacing the stub)
Production-grade configuration/secrets/logging (Key Vault, real deploy pipeline)
Waypoint and Relay begin building against this
Production tier (planned now, built incrementally):
MFA (TOTP)
Rate limiting on login/registration/reset
Real audit logging
Session/token revocation
Uptime monitoring and alerting
Tested backup/recovery for the credential store
Admin app — identity-administration surface
RoundTrip migration (gated, last):
Tested account-recovery flow
Rollback path
"Set up your new login" user communication
The actual cutover
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | September 2026 | Pete Carroll | Initial SAD. Sections 3-6 (structure, data model proven portions, token-issuance mechanics, Entra/RoundTrip/Waypoint integration) describe what the spike actually proved. Sections 5's entitlement-caching design, 7-9 (security model, testing, deployment), and the production-tier data model in Section 4 are new proposals for the full production-tier scope Pete chose 2026-09-06. |