Services
Dependency graph
| Service | Calls | Why |
|---|---|---|
api-audit | — | Depends on nothing. Everything depends on it. |
api-account | api-audit | To record changes |
api-identity | api-account, api-audit | To read the account behind a login or key; to record mints |
api-libraryset | api-account, api-audit | To put names to the user IDs stamped on records; to record changes |
ui-auth | api-identity, api-account, api-audit, api-libraryset | The login flow |
ui-management | api-account, api-audit, api-libraryset | Administration |
Discovery is Aspire's, declared with WithReference in the AppHost and consumed through the
DiscoverableServices option on AddCSMCommonWeb. No service holds another's URL in configuration.
api-libraryset
The reason the product exists. Tag groups, tags, library sets, variables, values, resolution and import.
Two pieces of logic sit outside the endpoints:
VariableConsumptionHandler— the resolution rule.GetWinningValuesreturns the whole winning value per variable;GetForConsumptionreduces that to a name-to-string map. Both consumption endpoints go through it, so the two cannot disagree about which value wins.LibraryImportPlanner— works out what an import would do before anything is written, so a rejected file is refused before the transaction has changed anything.
It sets IncludeRecordAuthors, which is what wires up the calls to the account API for author names.
It also sets IncludeCreditMetering. GET /variables/{id}, GET /variablesets/{id}, GET /variablevalues and
GET /variablevalues/preview carry .RequireCredits(...): before one runs, the service reads the balance from the account API's
GET /organizations/me/credits (refusing with 429 if it cannot be afforded), and after a 2xx it publishes a
$1 GET /export is metered by ExportCreditMeter
instead, because its cost is one credit per library set exported. See Credits.
api-account
Organizations, users, groups, permissions and API keys.
IdentityProviderVerifier— puts a set of credentials to Entra or Google without storing them. On its ownHttpClientwith a 15-second timeout: checking credentials is something somebody is waiting on, and a provider that has gone quiet should say so rather than hold the page.ApiKey— generating and hashing a key holds no state, so a singleton serves every request.GroupPermissionRules— the grant checks and dependency checks applied when a group is saved.AccountPatternRules— decides whether an email address may have an account created for it.- Credits — serves
GET /organizations/me/credits(used and total credits for the caller's organization) and consumes thecredits-spentqueue, adding each message'sAmountto the organization's used credits. A subscription payment (PAYMENT.SALE.COMPLETED) resets used credits to 0. See Credits.
Enum values are serialised by name (JsonStringEnumConverter) so a payload reads for itself.
api-identity
The only service that signs tokens.
JwtGenerator mints from three starting points — a login, a refresh, an API key — through one private
MintToken, so a caller gets the same session whichever route they came by. A second copy of that would
eventually mint subtly different sessions.
JwkGenerator produces signing key pairs. Operational, run out of band.
Every mint writes a JWT audit entry.
api-audit
The change log. The only service with no dependencies.
AuditVisibilityRulesdecides how much of the log a caller sees — the organizational and global split described in the audit API.AuditLoggingOptions.AuthNotRequiredis the allow-list of entry types that may be written without a JWT. That list is this endpoint's security boundary.
It configures a permissive CORS policy, because the management UI reads it directly from the browser.
ui-auth
Blazor Server. The login page, branded per organization.
| Piece | Does |
|---|---|
OrganizationResolver / CurrentOrganizationMiddleware | Works out which organization a request is signing in to |
OrganizationSchemeProvider | Registers an OIDC scheme per organization per provider, resolved at request time |
ProviderSelector | Offers whichever providers the organization has configured |
EntraPhotoSource | Fetches a profile photo where the provider offers one |
TokenGenerator / TokenCookies | Calls the identity API and writes the x-tagshape-auth cookie |
CustomCssRules | Filters an organization's custom CSS against an allow-list |
Where nothing in front of the deployment sets the organization per request, the x-tagshape-org header names it.
ui-management
Vue 3 with vue-router and Monaco. The only non-C# part of the product, and the one place CLAUDE.md carves out
from the C#-everywhere rule.
Client-side services mirror the server's concepts: permissions.js reads the catalogue from
GET /permissions, token-refresh.js renews the session in the background, current-user.js holds the signed-in
account.
See The management UI.
Health and telemetry
Every service is wired by AddCSMCommonWeb with OpenTelemetry (named per service), Serilog and OpenAPI at
/openapi/v1.json with a JWT auth option. Health check endpoints exist at /healthz/startup; the AppHost's
health-check registrations are currently commented out.