Architecture
Four HTTP services and two front ends, orchestrated locally by .NET Aspire.
┌──────────────────┐
browser ───────────▶│ ui-auth │ Blazor — login, branded per organization
└ ────────┬─────────┘
│
┌────────▼─────────┐
│ api-identity │ mints and renews JWTs
└────────┬─────────┘
│
browser ──▶ ui-management ───┼──────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│api-account│ │api-audit │ │api-libraryset│
└─────┬────┘ └─────┬────┘ └──────┬───────┘
│ │ │
┌─────▼────┐ ┌─────▼────┐ ┌──────▼───────┐
│ Accounts │ │ Audit │ │ LibrarySets │ SQL Server
└──────────┘ └──────────┘ └──────────────┘
The services
| Service | Aspire name | Owns |
|---|---|---|
| Library set API | api-libraryset | The library and its resolution |
| Account API | api-account | Organizations, users, groups, permissions, keys |
| Identity API | api-identity | Token minting and renewal |
| Audit API | api-audit | The change log |
| Auth UI | ui-auth | Login (Blazor Server) |
| Management UI | ui-management | Administration (Vue 3) |
See Services for their dependencies and Data stores for what each persists.
Principles the code holds to
One database per service. No service reads another's tables. Where one needs data another owns, it asks over
HTTP — which is why the library set API calls POST /users/names/lookup rather than joining to a users table.
The JWT carries everything needed to authorise. Organization, subject and permissions all travel on the token, so no service has to ask another who is calling. Only the identity API talks to the account API about identity, and only when minting.
Authorisation is one piece of middleware. PermissionsMiddleware in TagShape.Common enforces
[RequiresPermissions] for every service. There is no second implementation to drift.
Organization scoping is applied at the query. Every query filters on the org claim, so an ID from another
organization reads as absent rather than forbidden.
Auditing is a shared handler, called over HTTP. IAuditHandler in TagShape.Common is how every service
writes to the audit API. That the audit API is reached over the network is why import writes one entry rather than
hundreds.
Shared behaviour lives in TagShape.Common. Telemetry, logging, OpenAPI, service discovery, JWT reading,
permission checking and the audit handler are all wired by one pair of extension methods —
AddCSMCommonWeb and UseCSMCommonWebTop/UseCSMCommonWebBottom. See
Shared libraries.
Local development
The Aspire host (TagShape.Product.AppHost) starts everything, wires service discovery and opens the dashboard:
dotnet run --project .\src\TagShape.Product.AppHost\
The two JavaScript apps are started through Aspire's JavaScript app support with Yarn.
x-tagshape-org names the organization the auth UI signs in to where nothing in front of the deployment sets it
per request; the AppHost sets it as an environment variable.
Testing
Test-driven, xUnit, one test class per class (UserService → UserServiceTests), test names in
<Function>_<ExpectedOutcome>_<Condition> form — for example
GenerateKey_ReturnPublicPrivatePemAndJwt_WhenCalled.
Test harness projects (TagShape.Common.TestHarness.API and .Web) supply the shared scaffolding.