Skip to main content

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

ServiceAspire nameOwns
Library set APIapi-librarysetThe library and its resolution
Account APIapi-accountOrganizations, users, groups, permissions, keys
Identity APIapi-identityToken minting and renewal
Audit APIapi-auditThe change log
Auth UIui-authLogin (Blazor Server)
Management UIui-managementAdministration (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 (UserServiceUserServiceTests), 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.