Skip to main content

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.

RiskMitigationStatus
Credential-stuffing / brute-force against loginRate limiting on login/reset endpoints (PRD §3.13)Proposed, production tier
A leaked or stolen token/session stays validReal revocation mechanism (PRD §3.10)Proposed, production tier
Account takeover via a stolen or guessed password aloneMFA (PRD §3.12)Proposed, production tier
Silent account merging via a spoofed or coincidentally-matching emailVerified-email-required linking, explicit confirm-link step, never automatic✅ Proven in spike
Forged or tampered tokensStandard 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 misuseReal audit logging (PRD §3.14, data model Section 4)Proposed, production tier
Credential-store data lossTested 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 traffick6 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 ExternalLogin really loads as an owned entity with no explicit Include(), whether the Account.Email unique 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.

ResourceProposed Name
Resource Grouprg-keystone-production
App Serviceapp-keystone-production
Key Vaultkv-keystone-pro
Custom Domainidentity.traxsgroup.com
DatabaseAzure SQL, own instance — never shared with any product or with Command Center

Key Vault secrets (proposed):

SecretPurpose
Entra--ClientId / Entra--ClientSecretOptional federation app registration
ConnectionStrings--DefaultConnectionCredential store
CommandCenter--ApiKeyOutbound entitlement calls
SendGrid--ApiKeyPassword-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:

  1. 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.
  2. 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​

VersionDateAuthorChanges
1.0September 2026Pete CarrollInitial 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.