Command Center API — Solution Architecture Document
Version: 3.0 Date: September 2026 Author: Pete Carroll, Founder — Traxs Group LLC (with Claude) Status: Draft — reflects actual implementation, not the original plan
What changed since v2.0, and why
v2.0 (September 3) reflected the schema and the API-key authentication scheme, written the moment those two things were done. Nothing built since then was in it — three real endpoints, a real health check, and a full three-layer test suite across all of it.
- Section 6 is new — documents
GET /v1/entitlements,GET/POST/PATCH /v1/subscriptions, andGET /v1/suite/subscriptions, all built, all API-key gated, all verified with unit, integration, and functional tests. None of this existed in any form in v2.0. GET /healthis real now. Section 7 replaces v2.0's silence on this — the endpoint used to return200unconditionally; it now runs a real database connectivity check via ASP.NET Core's Health Checks framework.- Section 8 is new — documents the testing strategy this project settled into across three features: unit tests (mocked repositories) for logic, integration tests (a real, shared SQL Server container) for SQL translation and schema constraints, functional tests (a real running app via
WebApplicationFactory) for the full HTTP pipeline including auth. This wasn't planned in advance; it emerged from building entitlements first and proved valuable enough to repeat deliberately for every feature after. - Section 3 (Solution Structure) is updated to match the actual tree — new
Common/Pricing/(a shared effective-price service, extracted specifically to keep entitlements and subscription-creation from silently disagreeing on price), newFeatures/Entitlements/Subscriptions/Suite/folders, eight Specifications now instead of zero, and the oldHealthEndpoint.csFastEndpoints class removed entirely in favor of the real health check middleware. - PRD staleness corrected, not just noted. §3.10's documented implementation ("queries Stripe for active subscriptions") stopped being true the moment the suite-subscriptions endpoint was actually built against Command Center's own
Subscriptiontable instead — this SAD's Section 6.3 reflects what's actually there. The PRD itself is being corrected in the same pass as this document. - Sections 9/10 (RoundTrip Client, RoundTrip Internal Endpoints) are untouched — still accurately describing planned, unbuilt work.
1. Architecture Overview
The Command Center API is a .NET 10 API built with Clean Architecture: five projects under src/ (SharedKernel, Core, UseCases, Infrastructure, Web) plus a parallel tests/ tree, matching RoundTrip's own solution layout — the whole point of ADR-007 being that any RoundTrip developer can navigate this codebase immediately.
What's actually live today: a real, SQL Server-backed schema for subscriptions, entitlement, and rate-lock commitments; three real service-to-service endpoints reading and writing that schema; the original SendGrid pass-through for contact/support/feedback; a real health check; and a full automated test suite covering all of it at three levels of fidelity.
2. System Context
┌─────────────────────────────────────────────────────────────────────┐
│ COMMAND CENTER API CONSUMERS │
│ │
│ Browser Forms Product frontends/ RoundTrip API │
│ (roundtrips.app webhook handlers (client not yet │
│ traxsgroup.com) (entitlements, built — Section │
│ subscriptions, 9) │
│ suite switcher) │
└──────────┬──────────────────┬────────────────────┬──────────────────┘
│ HTTPS (public) │ HTTPS + API Key │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ COMMAND CENTER API │
│ api.traxsgroup.com │
│ Azure App Service — app-commandcenter-production │
│ .NET 10 · FastEndpoints · Cyrus Mediator │
│ Real SQL Server-backed schema (Section 5) │
│ Real entitlements/subscriptions/suite (Section 6) │
└──────────┬───────────────────────┬──────────────────────────────────┘
│ │
┌──────▼──────┐ ┌───────────▼──────────────────────────┐
│ SENDGRID │ │ ROUNDTRIP API │
│ Email │ │ api.roundtrips.app │
│ delivery │ │ (client not built yet — Section 9) │
└─────────────┘ └───────────────────────────────────────┘
│
┌──────▼──────┐
│ SQL SERVER │
│ (Azure SQL │
│ in prod; │
│ Docker via │
│ OrbStack │
│ locally) │
└─────────────┘
3. Solution Structure
CommandCenterAPI/
CommandCenterAPI.slnx
Directory.Build.props
Directory.Packages.props
src/
CommandCenter.API.SharedKernel/
EntityBase.cs · IAggregateRoot.cs · ValueObject.cs
IRepository.cs · IReadRepository.cs · IUnitOfWork.cs
LoggingBehavior.cs
CommandCenter.API.Core/
Aggregates/
PlatformContext/
Tenant.cs
Specifications/
TenantByIdSpec.cs
SubscriptionContext/
Product.cs · ProductTier.cs · TierPrice.cs
Subscription.cs · PriceCommitment.cs · PriceCommitmentLine.cs
Specifications/
ActiveProductsSpec.cs · ProductBySlugSpec.cs · ProductByIdSpec.cs
ProductsByIdsSpec.cs · ProductTierByIdSpec.cs
ProductTierByProductAndNameSpec.cs · ProductTiersByIdsSpec.cs
SubscriptionByIdSpec.cs · SubscriptionByTenantAndProductSpec.cs
SubscriptionsByTenantSpec.cs · ActivePriceCommitmentsByTenantSpec.cs
ValueObjects/
TenantId.cs · ProductId.cs · TierId.cs · TierPriceId.cs
SubscriptionId.cs · PriceCommitmentId.cs · PriceCommitmentLineId.cs
Enums/
ContactRequestType.cs · SupportProduct.cs · SupportPriority.cs
FeedbackCategory.cs · ResultType.cs · SubscriptionStatus.cs
Interfaces/
IEmailService.cs
Models/
ContactRequest.cs · EmailMessage.cs
Exceptions/
DomainException.cs
CommandCenter.API.UseCases/
Result.cs
Common/
Behaviors/
TransactionBehavior.cs · ValidationBehavior.cs
Pricing/
IEffectivePricingService.cs · EffectivePricingService.cs
Features/
Contact/ · Support/ · Feedback/ (Command/Handler/Validator triads)
Entitlements/
GetEntitlementsQuery.cs · GetEntitlementsHandler.cs
Subscriptions/
GetSubscriptionsQuery.cs · GetSubscriptionsHandler.cs
CreateSubscriptionCommand.cs · CreateSubscriptionHandler.cs
CreateSubscriptionValidator.cs
UpdateSubscriptionStatusCommand.cs · UpdateSubscriptionStatusHandler.cs
UpdateSubscriptionStatusValidator.cs
Suite/
GetSuiteSubscriptionsQuery.cs · GetSuiteSubscriptionsHandler.cs
MediatorRegistration.cs
CommandCenter.API.Infrastructure/
Data/
AppDbContext.cs · UnitOfWork.cs · EfRepository.cs
Config/
TenantConfiguration.cs · ProductConfiguration.cs
ProductTierConfiguration.cs · SubscriptionConfiguration.cs
PriceCommitmentConfiguration.cs
Migrations/
Extensions/
InfrastructureServiceExtensions.cs
Services/
SendGridEmailService.cs
CommandCenter.API.Web/
Authentication/
ApiKeyAuthenticationOptions.cs · ApiKeyAuthenticationHandler.cs
Configurations/
ServiceConfigs.cs
Endpoints/
Contact/ · Support/ · Feedback/
Entitlements/
GetEntitlementsEndpoint.cs
Subscriptions/
GetSubscriptionsEndpoint.cs · CreateSubscriptionEndpoint.cs
UpdateSubscriptionStatusEndpoint.cs
Suite/
GetSuiteSubscriptionsEndpoint.cs
Extensions/
ResultExtensions.cs · ValidationExceptionExtensions.cs
Middleware/
GlobalExceptionMiddleware.cs
Program.cs
appsettings.json
appsettings.Development.json
tests/
CommandCenter.API.Architecture.Tests/
CommandCenter.API.UnitTests/
Features/Entitlements/ · Features/Subscriptions/ · Features/Suite/
CommandCenter.API.IntegrationTests/
Fixtures/DatabaseFixture.cs
Data/SchemaConstraintTests.cs
Specifications/SpecificationTests.cs
Features/Entitlements/ · Features/Subscriptions/ · Features/Suite/
CommandCenter.API.FunctionalTests/
Fixtures/CommandCenterApiFactory.cs
Entitlements/ · Subscriptions/ · Suite/
Notable structural decisions, updated for what's actually here now:
SharedKernelsits belowCore,Infrastructuredeliberately does not depend onUseCases(stricter than RoundTrip's own architecture tests, corrected on purpose) — both unchanged from v2.0, still enforced byArchitecture.Tests.Common/Pricing/EffectivePricingServiceis the one piece of genuinely shared business logic across features — "which price applies" (a tier's current rate, unless an activePriceCommitmentline locks a different one) is identical forGetEntitlementsHandlerandCreateSubscriptionHandler/UpdateSubscriptionStatusHandler, extracted specifically so the two could never silently drift apart. Registered inWeb'sServiceConfigs.cs, notInfrastructure, sinceInfrastructurecan't referenceUseCases.- Eight Specifications, all under
Core/Aggregates/<Context>/Specifications/, all built incrementally as each feature needed them — none were designed speculatively ahead of a real need. GetSuiteSubscriptionsQuery/HandlerreadsIConfigurationdirectly, matching the existing pattern inSubmitContactHandleretc. — the per-product app URLs it returns live inappsettings.json'sSuiteUrlssection, not a new database column.HealthEndpoint.csno longer exists./healthis now ASP.NET Core's own Health Checks middleware (app.MapHealthChecks("/health")), not a FastEndpoints class.
4. Authentication Implementation
Unchanged from v2.0 — ApiKeyAuthenticationHandler, registered as a real ASP.NET Core authentication scheme, remains the mechanism gating every service-to-service endpoint. All three new endpoints in Section 6 use it the same way entitlements originally proved it out: none of them call AllowAnonymous().
5. Persistence Layer
Unchanged from v2.0 in substance — the schema itself didn't change. See v2.0's content (preserved in version history) for AppDbContext, the seven-table schema, FK/cascade design, UnitOfWork/EfRepository, and local development via Docker Compose. What's different now is that this schema has real consumers — Section 6 documents what actually reads and writes it.
6. Built Features — Entitlements, Subscriptions, Suite Subscriptions
New in this version. None of this existed when v2.0 was written.
6.1 GET /v1/entitlements
Per BRD §5.3. Returns per-product entitlement for a tenant — every active product gets an entry, not just ones the tenant has subscribed to, so a caller asking "is this tenant entitled to Waypoint" gets a real false rather than a missing entry.
{
"tenantId": "...",
"entitlements": [
{ "productSlug": "roundtrip", "isEntitled": true, "tier": "Standard", "effectivePrice": 79.00 },
{ "productSlug": "waypoint", "isEntitled": false, "tier": null, "effectivePrice": null }
]
}
IsEntitled (on the Subscription entity itself) treats Active, Trial, and PastDue as entitled — a deliberate grace-period judgment call, not something explicitly specified anywhere, matching how Stripe's own past_due typically still grants access during dunning. EffectivePrice comes from IEffectivePricingService (Section 3). A subscription referencing a tier that no longer exists fails that one product closed rather than failing the whole response — a data integrity problem shouldn't take down entitlement checks for every other product a tenant is correctly subscribed to.
6.2 GET/POST/PATCH /v1/subscriptions
Per BRD §5.2.
GET /v1/subscriptions?tenantId={id}— lists a tenant's subscriptions with resolved product slug and tier name (not raw internal IDs).POST /v1/subscriptions— identifies the product/tier by slug/name, not internalGuids, since the caller (each product's own Stripe webhook handler) knows its own plan names, not Command Center's internal IDs. Checks for an existing subscription for that(Tenant, Product)first and returns409 Conflict— confirmed design: one persistent row per(Tenant, Product), a resubscription updates the existing row viaPATCH, it never gets a secondPOST-created row. Returns201with the created subscription's status and effective price — the concrete form of BRD §5.2's "checks for an active PriceCommitment line... first."PATCH /v1/subscriptions/{id}— maps a target status string to the matchingSubscriptionentity method (Activate()/MarkPastDue()/Suspend()/Cancel()).Trialis deliberately rejected as a valid target —Subscription.Create()is the only legitimate way a subscription ever starts inTrial; there's no entity method to transition into it from any other state. The entity's own guards do the real enforcement (e.g.Activate()throwing on an already-Cancelledsubscription, surfacing as a422viaGlobalExceptionMiddleware) — the handler just routes to the right method and lets the guard fire.
This is the first real write path in Command Center — the first place TransactionBehavior wraps a transaction that actually does something, not a no-op around a read.
6.3 GET /v1/suite/subscriptions
Per PRD §3.10 — built against Command Center's own Subscription table, not Stripe directly, correcting the PRD's stale July-era implementation note (written before the schema existed at all). Feeds the suite switcher UI:
{
"tenantId": "...",
"subscriptions": {
"roundtrip": { "active": true, "plan": "Standard", "url": "https://app.roundtrips.app" },
"relay": { "active": false, "plan": null, "url": "https://traxsrelay.app" }
}
}
active reuses the same IsEntitled semantics as entitlements (§6.1) — a trialing or briefly past-due customer should still see the product in their suite switcher. url is populated regardless of active status, per the PRD's own documented example — it's static per-product configuration (appsettings.json's SuiteUrls), not tenant data, so an inactive product still carries its known app URL. A product slug with no configured URL returns null rather than throwing, so a newly-added product doesn't break this endpoint before someone remembers to configure it.
7. Health Checks
New in this version. GET /health used to return 200 unconditionally regardless of whether anything downstream actually worked. It now uses ASP.NET Core's real Health Checks framework:
services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
registered in InfrastructureServiceExtensions, colocated with AppDbContext's own registration. This calls EF Core's CanConnectAsync() under the hood — /health now genuinely reflects database reachability. Wired into the pipeline via app.MapHealthChecks("/health"), not a FastEndpoints class (the old HealthEndpoint.cs is removed) — public by default, matching the previous AllowAnonymous() behavior, since a load balancer or uptime monitor shouldn't need an API key.
This is deliberately the smaller half of TRA-386 (see command-center-framing.md §8's 2026-09-06 addendum for the full scoping). The broader admin-app ops/financial dashboard TRA-386 also names is a separate, larger, still-open question — dependent on real product input on what's actually useful to see, not something to design blind ahead of the admin app itself existing.
8. Testing Strategy
New in this version — this wasn't planned in advance, it emerged from building GET /v1/entitlements first and proved valuable enough to repeat deliberately for every feature since. Three layers, each catching a different class of problem:
- Unit tests (
CommandCenter.API.UnitTests) —IReadRepository<T>/IRepository<T>mocked with NSubstitute, no database. Cheapest, fastest, and where every deliberate judgment call gets pinned down explicitly (e.g.PastDuecounting as entitled,Trialbeing rejected as a PATCH target) so a future change to that behavior is a visible, deliberate decision rather than a silent regression. - Integration tests (
CommandCenter.API.IntegrationTests) — a single SQL Server container shared across the whole test run (DatabaseFixture, via[CollectionDefinition("Database")]) rather than one per test or per class. Catches what mocked unit tests structurally can't: whether a Specification's LINQ actually translates to correct SQL, whether owned entities (TierPrice,PriceCommitmentLine) really do load automatically with no explicitInclude(), and whether the database's own constraints (theSubscriptionunique index, the FKRestrictbehavior) are actually enforced, not just documented. - Functional tests (
CommandCenter.API.FunctionalTests) — a real running instance of the app viaWebApplicationFactory<Program>, backed by its own shared SQL Server container (CommandCenterApiFactory, via[CollectionDefinition("Api")]), making real HTTP requests. The only layer that proves the full pipeline works together — routing, the API key scheme,ValidationBehavior,GlobalExceptionMiddleware— not just the logic in isolation.
Two real, non-obvious things this process surfaced, worth recording since they're the kind of interaction that's easy to hit again elsewhere:
WebApplicationFactory<Program>requiresProgramto be public — top-level statements generate itinternalby default, fixed with a trailingpublic partial class Program;inProgram.cs.WebApplicationFactoryanddotnet efboth rely onHostAbortedExceptionescapingProgram.Mainuncaught as their success signal — code that catches and logs it (even just to avoid a noisy "Fatal" log during migrations) breaks functional testing.Program.csnow rethrows after logging, for exactly this reason. Serilog'sUseSerilog(..., preserveStaticLogger: true)was also needed — without it, the same host-resolution mechanism triggers a "logger is already frozen" error, a documented conflict between Serilog's bootstrap-logger pattern andWebApplicationFactory's internal host construction.
9. RoundTrip Client Implementation (New endpoints needed in RoundTrip API)
Unchanged from v2.0 — still planned, not built. The Command Center API will need IRoundTripClient (Core), RoundTripClient (Infrastructure, HttpClientFactory, 10-second timeout, graceful null-return on failure), and matching internal endpoints on RoundTrip's own side. None of this exists yet.
public sealed class RoundTripClient : IRoundTripClient
{
private readonly HttpClient _http;
private readonly ILogger<RoundTripClient> _logger;
public async Task<RoundTripRevenueSummary?> GetRevenueSummaryAsync(
Guid tenantId, string period, CancellationToken ct)
{
try
{
var response = await _http.GetAsync(
$"/v1/internal/revenue-summary?tenantId={tenantId}&period={period}", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RoundTripRevenueSummary>(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get revenue summary from RoundTrip for tenant {TenantId}", tenantId);
return null; // Graceful degradation — Waypoint handles null
}
}
}
10. RoundTrip Internal Endpoints (New endpoints needed in RoundTrip API)
Unchanged from v2.0 — still planned, not built.
| Endpoint | What It Returns |
|---|---|
GET /v1/internal/revenue-summary | MTD/YTD revenue from paid invoices |
GET /v1/internal/tickets/open | Open ticket count, status breakdown, estimated value |
GET /v1/internal/invoices/recent | Last N invoices with status |
GET /v1/internal/team | Active TenantUsers with roles |
GET /v1/internal/ar/aging | Outstanding invoices by age bucket |
All internal endpoints: require X-Internal-ApiKey header, include tenantId as a query parameter, return 404 if tenant not found, return data scoped to the specified tenant. Tracked as Linear tickets under TRA-NNN when Waypoint M7 is reached.
11. Infrastructure
11.1 Azure Resources (Current)
| Resource | Name |
|---|---|
| Resource Group | rg-commandcenter-production |
| App Service | app-commandcenter-production |
| Key Vault | kv-commandcenter-pro |
| Custom Domain | api.traxsgroup.com |
11.2 Key Vault Secrets (Current + Planned)
| Secret | Purpose | Status |
|---|---|---|
SendGrid--ApiKey | Email delivery | ✅ Live |
CommandCenter--ApiKey | Inbound service-to-service auth | ⚠️ Code is ready to consume this (Section 4) — the secret itself still needs to actually exist in kv-commandcenter-pro. Local dev and tests use their own separate values, never this real secret. |
ConnectionStrings--DefaultConnection | SQL Server connection string | ⏳ Needed once this deploys anywhere beyond local dev — not yet added anywhere real |
RoundTripApi--BaseUrl | RoundTrip API URL | ⏳ Add when Section 9 gets built |
RoundTripApi--InternalApiKey | Outbound auth to RoundTrip | ⏳ Add when Section 9 gets built |
Stripe--SecretKey | Suite subscription queries | ⏳ No longer needed for this purpose — §6.3 confirms Suite Subscriptions reads Command Center's own database, not Stripe directly. Revisit if a genuine Stripe integration need arises elsewhere. |
11.3 Application Configuration (New)
| Key | Purpose |
|---|---|
SuiteUrls:<slug> | Per-product app URL returned by GET /v1/suite/subscriptions (§6.3) — static config, not a database column. Must be kept in sync with real Product.Slug values; nothing enforces this automatically. |
11.4 CORS Configuration
Unchanged from v2.0. Service-to-service calls don't go through a browser, so CORS doesn't apply to them — it only matters for the public, browser-facing endpoints.
12. Graceful Degradation
Unchanged in principle from v2.0 — still non-negotiable, still nothing built yet that exercises it for real across services. The Restrict delete behaviors in Section 5 protect data integrity within Command Center's own database; that's a different concern from graceful degradation across services, which is still tied to Section 9's not-yet-built RoundTripClient.
13. Growth Path
Current (September 2026):
Contact/Support/Feedback → SendGrid
Real schema: Tenant/Product/ProductTier/TierPrice/Subscription/
PriceCommitment/PriceCommitmentLine — migrated, verified
Service-to-service API key authentication — real scheme, implemented
GET /v1/entitlements, GET/POST/PATCH /v1/subscriptions,
GET /v1/suite/subscriptions — all built, all tested three ways
Real health check (SQL Server connectivity)
Local dev: Docker Compose (SQL Server via OrbStack)
Next:
TRA-386's broader piece — the admin-app ops/financial dashboard content,
genuinely open pending real product input on what it should show
Admin app itself — not started in any form yet
Waypoint launch:
+ RoundTrip data endpoints (5 endpoints, Section 9/10)
+ API key authentication for those endpoints (infrastructure already exists —
just needs the endpoints built behind it)
Relay launch:
+ Lead sync endpoint (Relay → RoundTrip)
+ Client sync endpoint (bidirectional)
+ Campaign trigger events
2028+:
+ Unified billing hub (Stripe orchestration)
+ AI inference gateway (Anthropic API routing)
+ Webhook management (event bus)
+ Platform analytics
+ Domain events (once there's a real first one to raise — see Section 5.3
in v2.0's preserved content)
14. Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | July 2026 | Pete Carroll | Initial draft |
| 2.0 | September 2026 | Pete Carroll (with Claude) | Full rewrite reflecting the real schema and the real ApiKeyAuthenticationHandler scheme. |
| 3.0 | September 2026 | Pete Carroll (with Claude) | Added Section 6 (entitlements/subscriptions/suite subscriptions, all built and tested), Section 7 (real health check replacing the old no-op), Section 8 (the three-layer testing strategy that emerged during this build). Corrected the PRD-inherited stale note that Suite Subscriptions queries Stripe directly — it reads Command Center's own database. Updated Section 3 (solution structure) and Section 11 (Key Vault/config) to match what's actually in the codebase. Sections 9/10 (RoundTrip Client, RoundTrip Internal Endpoints) intentionally left unchanged — still accurately describing unbuilt work. |