The Cardinal application

The back end.

For: developers

Cardinal's back end is a typed API on Azure Functions. It splits cleanly into reads and mutations, authenticates to the database without a password, and writes only through the same stored procedures the agents use โ€” so the app and the ring share one write discipline.

tRPC on Azure Functions Read / mutation split Keyless to Postgres

The stack

ConcernChoice
API styletRPC server โ€” typed procedures, shared types with the SPA
HostAzure Functions (Flex Consumption, Node)
Database driverThe PostgreSQL client library
IdentityManaged-identity credential + token handling
ValidationSchema validation on inputs and outputs
TriggerA single HTTP trigger routes all API calls

The read / mutation split

The API is divided into two router families. Reads have no side effects โ€” they shape data for the views. Mutations are the audited, idempotent actions โ€” the human's Publish, Approve, Ratify, Assign. The split is structural, so it's always clear whether a call can change state.

Reads

  • No side effects
  • Query the base tables, shape for the UI
  • Cached on the client

Mutations

  • Audited and idempotent
  • Carry a deterministic operation id so a retry lands once
  • Go through stored procedures, never raw inserts
  • Reject non-human callers on human-only actions

Reaching the database โ€” keyless

The API authenticates to Postgres with a user-assigned managed identity: it exchanges a short-lived token rather than presenting a password, then adopts a least-privilege database role for the session. There is no database password in the code, the configuration, or the deployment pipeline.

One write-path, two callers. The same stored procedures that enforce the actor guard, idempotency, and the hash-chained audit row are called by the app on behalf of a human and by the agents on their own behalf. That shared discipline is why the two halves never diverge. See Data Model for the write-path contract.

The conversation substrate

Behind the Conversations view is a substrate that groups inbound and outbound messages into threads. Short summaries are embedded into vectors using a shared embeddings endpoint, and a classifier links each new message to the right conversation โ€” deterministically where it can, and with a focused model call where it can't. The bodies of messages are handled with the same sensitivity discipline as everywhere else.

The data model & write-path โ†’ ยท Where it runs โ†’