Skip to main content
Use Postgres when workers on more than one host must share one store, because BEGIN IMMEDIATE is a write lock on a local file and does not reach across hosts. The store is the same protocol, graded by the same suite as SQLite; what changes is a URL. Prerequisites: a Postgres 14 or later server, a database, a role for CTRLRun, and pip install "ctrlrun[postgres]".
1

Install and connect

The URL goes to psycopg.connect unchanged, so ?sslmode=require, ?connect_timeout=5, a service= name and the PG* environment variables all work. Put the password in ~/.pgpass or PGPASSWORD, not in the URL. On the command line the schema travels as CTRLRun’s own query parameter:
2

Create the schema and grant the role

The store does not create the schema, so a typo cannot become a second, empty, authoritative-looking store. Create it once, and give the role what it needs:
CREATE on the schema is what migrations need, and it is not optional: the store migrates at open, so a role without it cannot run the CLI at allctrlrun receipts --verify-chain is refused before a receipt is read, naming the missing privilege. A read-only role with USAGE and SELECT is for your own queries against the tables. The database’s encoding must be UTF-8; a lossy encoding is refused at open, because a receipt that cannot store the argument it records is not a receipt.
3

Let the first writer migrate

The schema is versioned. A writer opens the store, reads schema_version, and applies any migration forward, once, under a lock, so concurrent starts do not race. There is no flag that opens a database un-migrated. A binary older than the schema refuses immediately with SchemaMismatch, before reading any other table, so upgrade every reader before any writer.A read command (receipts, effects, inspect, stats, approve, deny, resolve) migrates nothing: it opens only a database already at head, and names the missing migration otherwise.
4

Understand the one new ambiguity

A connection lost during COMMIT is the case worth naming: Postgres very often did commit, and the client cannot know. The store treats it as unknown and re-reads the row to find out which, and only if the re-read itself fails does it refuse to proceed. FAILED is reserved for an abort the server stated. Nothing here changes what AMBIGUOUS means for the remote your executor called; that path is unchanged.

Verify against it

ctrlrun verify --store-url postgresql://… runs the guarantee catalogue against a scratch schema it creates and drops, never against yours. The store conformance suite grades your database with the same cases SQLite passes. It is not a command — it is a function, because it takes a live backend:

Throughput

Every reservation queues on one row per effect key and receipts append behind one chain head, so the store serializes writes the way a ledger does. That is the design: a hundred agents refunding a hundred different payments proceed in parallel; a hundred agents refunding the same payment are one winner and ninety-nine refusals.

If it didn’t work

  • schema "ctrlrun" does not exist: create it; the store will not.
  • SchemaMismatch: the database is at version N and this binary understands M: upgrade the binary, or downgrade nothing; there is no backward migration.
  • encoding ... is not UTF8: create the database with ENCODING 'UTF8'.
  • permission denied for schema: the role lacks CREATE on the first open.

Next