Skip to main content

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.

  1. Section 6 is new — documents GET /v1/entitlements, GET/POST/PATCH /v1/subscriptions, and GET /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.
  2. GET /health is real now. Section 7 replaces v2.0's silence on this — the endpoint used to return 200 unconditionally; it now runs a real database connectivity check via ASP.NET Core's Health Checks framework.
  3. 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.
  4. 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), new Features/Entitlements/Subscriptions/Suite/ folders, eight Specifications now instead of zero, and the old HealthEndpoint.cs FastEndpoints class removed entirely in favor of the real health check middleware.
  5. 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 Subscription table 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.
  6. 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:

  • SharedKernel sits below Core, Infrastructure deliberately does not depend on UseCases (stricter than RoundTrip's own architecture tests, corrected on purpose) — both unchanged from v2.0, still enforced by Architecture.Tests.
  • Common/Pricing/EffectivePricingService is the one piece of genuinely shared business logic across features — "which price applies" (a tier's current rate, unless an active PriceCommitment line locks a different one) is identical for GetEntitlementsHandler and CreateSubscriptionHandler/UpdateSubscriptionStatusHandler, extracted specifically so the two could never silently drift apart. Registered in Web's ServiceConfigs.cs, not Infrastructure, since Infrastructure can't reference UseCases.
  • 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/Handler reads IConfiguration directly, matching the existing pattern in SubmitContactHandler etc. — the per-product app URLs it returns live in appsettings.json's SuiteUrls section, not a new database column.
  • HealthEndpoint.cs no longer exists. /health is 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 internal Guids, 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 returns 409 Conflict — confirmed design: one persistent row per (Tenant, Product), a resubscription updates the existing row via PATCH, it never gets a second POST-created row. Returns 201 with 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 matching Subscription entity method (Activate()/MarkPastDue()/Suspend()/Cancel()). Trial is deliberately rejected as a valid target — Subscription.Create() is the only legitimate way a subscription ever starts in Trial; 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-Cancelled subscription, surfacing as a 422 via GlobalExceptionMiddleware) — 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. PastDue counting as entitled, Trial being 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 explicit Include(), and whether the database's own constraints (the Subscription unique index, the FK Restrict behavior) are actually enforced, not just documented.
  • Functional tests (CommandCenter.API.FunctionalTests) — a real running instance of the app via WebApplicationFactory<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> requires Program to be public — top-level statements generate it internal by default, fixed with a trailing public partial class Program; in Program.cs.
  • WebApplicationFactory and dotnet ef both rely on HostAbortedException escaping Program.Main uncaught as their success signal — code that catches and logs it (even just to avoid a noisy "Fatal" log during migrations) breaks functional testing. Program.cs now rethrows after logging, for exactly this reason. Serilog's UseSerilog(..., 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 and WebApplicationFactory'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.

EndpointWhat It Returns
GET /v1/internal/revenue-summaryMTD/YTD revenue from paid invoices
GET /v1/internal/tickets/openOpen ticket count, status breakdown, estimated value
GET /v1/internal/invoices/recentLast N invoices with status
GET /v1/internal/teamActive TenantUsers with roles
GET /v1/internal/ar/agingOutstanding 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)​

ResourceName
Resource Grouprg-commandcenter-production
App Serviceapp-commandcenter-production
Key Vaultkv-commandcenter-pro
Custom Domainapi.traxsgroup.com

11.2 Key Vault Secrets (Current + Planned)​

SecretPurposeStatus
SendGrid--ApiKeyEmail delivery✅ Live
CommandCenter--ApiKeyInbound 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--DefaultConnectionSQL Server connection string⏳ Needed once this deploys anywhere beyond local dev — not yet added anywhere real
RoundTripApi--BaseUrlRoundTrip API URL⏳ Add when Section 9 gets built
RoundTripApi--InternalApiKeyOutbound auth to RoundTrip⏳ Add when Section 9 gets built
Stripe--SecretKeySuite 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)​

KeyPurpose
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​

VersionDateAuthorChanges
1.0July 2026Pete CarrollInitial draft
2.0September 2026Pete Carroll (with Claude)Full rewrite reflecting the real schema and the real ApiKeyAuthenticationHandler scheme.
3.0September 2026Pete 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.