Architecture
How services store state, publish events, authorize requests, and call each other.
Every service owns one Postgres: Each service owns one Postgres database as the source of truth for its bounded context; services never read each other's databases. database. A Command: An HTTP POST that changes state, recorded with an Idempotency-Key so retries do not double-apply; not CQRS or a command bus. reads current state,
validates, writes the change, and appends an event row in one transaction. Queries are SELECT
handlers against the same database. Services call each other over HTTP/JSON.
Events use a Transactional outbox: Each domain inserts events into its own <domain>_events table in the same transaction as the state change; a publisher drains unpublished rows to Azure Service Bus.: each domain's <domain>_events table is
durable. Each domain has its own fanout topic. The broker is Azure Service Bus in all
environments (0016,
0041). The emulator runs locally.
We never read events to determine our own state.
Authentication and authorization
Authentication is the login ceremony (OIDC with the IdP via authn). It produces a
signed JWT.
Authorization happens at the service when it is called:
- Validate
Authorization: Bearer <jwt>— signature againstauthnJWKS, trust chain, verifiedsubbecomes the actor FID: The id of a principal. For a person it is their Kratos identity id, a lowercase UUID; for a service it is its OAuth2 client_id. It arrives as the verified sub claim of a Hydra access token and becomes the actor on the request.. - Evaluate Cedar: The authorization engine; each service embeds a Cedar policy and evaluates every mutating request against the actor's principal record. against that FID and the local principals copy.
A JWT is an authorization credential, not authentication. X-Dev-Actor is a temporary
local stand-in with no trust chain until JWT middleware ships.
See docs/decisions/0013-jwt-validated-in-each-service.md.
Commands and idempotency
Callers send an Idempotency-Key header. command.Execute claims the key, runs domain logic inside
one transaction, and stores the response payload for replay. No remote calls inside a transaction.
Pagination
List endpoints use bracket query parameters (pagination[page], pagination[pageSize]) and return
{ data, meta: { pagination } } inside the template envelope's payload. Create and get put the
record in payload.data as an object.
See docs/decisions/0014-pagination.md.
Identifiers
| Job | Type |
|---|---|
| Event ordering | bigserial (internal cursor) |
| Entity identity | UUIDv7 from uuid.NewV7() in Go |
| Principals | text FID |
Service layout
services/<name>/
cmd/ binaries (api, workers)
migrations/ goose SQL; 0001 is shared platform tables
queries/ sqlc input
db/ sqlc output (generated)
policies/ Cedar policy (embedded)
events/ public event payloads
messageBus/ generated topic names from topology.yaml
internal/ domain packages, handlers, authz, commandWhat we explicitly do not do
- Event sourcing or CQRS
- A runtime schema registry
- NATS, gRPC, or a service mesh (for now)
- Postgres extensions for UUID generation
Read the full binding document: docs/architecture.md.
Tracing
Shared HTTP middleware and configured internal clients carry W3C trace context. The template, accounting, billing, and leases APIs support opt-in OpenTelemetry export. Outbox headers preserve context for the shared Service Bus publisher and consumer. See HTTP and messaging tracing for process coverage and local verification.