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: Events are inserted into an events table in the same transaction as the state change; a publisher drains unpublished rows to RabbitMQ.: the events table is durable; RabbitMQ is
delivery. 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: A text principal identifier assigned upstream (for example Google Workspace). Becomes the actor on a request after JWT validation (verified sub claim); used in Cedar policies.. - 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 } } (nested inside the template envelope's payload when present).
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/ exchange bindings and consumer permissions
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.