Flow

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 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:

  1. Validate Authorization: Bearer <jwt> — signature against authn JWKS, trust chain, verified sub becomes 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..
  2. 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

JobType
Event orderingbigserial (internal cursor)
Entity identityUUIDv7 from uuid.NewV7() in Go
Principalstext 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, command

What 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.

On this page