Use Postgres for the run index
The proxy keeps a lookup index of runs (id, workflow, patient, event, creation
date, terminal status) in a small database, selected by the RUN_DB_URL
environment variable. By default this is a local SQLite file, which only
works for a single proxy instance. Switch to Postgres when you run more than
one proxy replica, so every replica reads and writes the same index. See
reference/run-store.md for what the index
contains and why it exists separately from Restate's durable state, and
explanation/sizing.md for when horizontal scaling
of the proxy is worth doing in the first place.
Before you start
- Only the proxy process reads
RUN_DB_URL— the endpoint never touches the run index (see reference/environment-variables.md). You do not need to change anything on the endpoint. - Losing or rebuilding this index never loses a run: the authoritative state (journal, suspension, resumption) lives in Restate, not here. See explanation/why-durable-execution.md.
- The schema (a single
runstable) is created automatically on boot — there is no migration script to run by hand, and nothing to provision beyond a reachable database.
1. Provision a Postgres database
Local stack (docker compose)
Postgres is already running: it has no compose profile, because the definition
store requires it (see
Manage workflow storage). So docker compose up -d,
with or without the stack profile, already started it:
docker compose --profile stack up -d --build
That is the postgres service (postgres:17-alpine, database clinical, user
clinical) defined in docker-compose.yml, persisted
in the pg-data volume and published on host port 5433 (POSTGRES_PORT) so it
cannot collide with another Postgres on the machine. The default password is
clinical; override it for anything beyond a throwaway local stack:
POSTGRES_PASSWORD=change-me docker compose --profile stack up -d --build
Pointing the run index at that same database is then just a matter of RUN_DB_URL
— the definition tables and runs are disjoint.
External / production Postgres
Provision a reachable Postgres instance (managed service or self-hosted) and
create an empty database plus a role with rights to create tables in it — no
further setup is required, the proxy issues its own CREATE TABLE IF NOT EXISTS and ALTER TABLE ... ADD COLUMN IF NOT EXISTS on first connection
(apps/api/src/server/run-store.ts). As with any credential, keep the
connection string out of the versioned instance manifest and inject it
through the environment (secret store, Kubernetes Secret, compose .env).
2. Point the proxy at it
Set RUN_DB_URL to a postgres:// (or postgresql://) connection string
for the proxy process:
RUN_DB_URL=postgres://clinical:clinical@postgres:5432/clinical \
docker compose --profile stack up -d --build
For a proxy running outside compose (bare process or another orchestrator), export the same variable before starting it:
export RUN_DB_URL=postgres://<user>:<password>@<host>:5432/<database>
pnpm --filter @meridian/api run server
Any URL starting with postgres:// or postgresql:// selects the Postgres
backend; anything else must start with sqlite: or the proxy refuses to boot
with an "unrecognized RUN_DB_URL" error. There is no partial/hybrid mode —
one proxy deployment uses exactly one backend at a time.
3. Restart the proxy
Restart every proxy replica with the new RUN_DB_URL so they all point at
the same Postgres database — this is the property that makes multiple
replicas safe (SQLite is a local file per replica; Postgres is shared).
Restarting the proxy is enough; you do not need to restart the endpoint or
Restate. See how-to/upgrade-the-platform.md for
the general proxy restart/rollout order.
4. Verify
Check that the proxy reports the run index as reachable:
curl http://localhost:4000/readyz
/readyz returns 200 only if both the Restate ingress and the run index
respond; on failure it returns 503 with a { checks } detail telling you
which one failed. Then confirm the index is actually being written to by
starting a run and listing it back through GET /api/runs, or by inspecting
the table directly:
psql "$RUN_DB_URL" -c 'select id, workflow_name, created_at from runs order by created_at desc limit 5;'
Notes on switching
- Switching backends does not migrate existing data: pointing
RUN_DB_URLat a fresh Postgres database starts with an emptyrunstable. If you need to keep the run history visible, back up the old SQLite file first — see how-to/back-up-and-restore.md — there is no built-in SQLite-to-Postgres migration tool. - Back up Postgres the same way as any other database in your stack
(
pg_dump); this index is convenience data, not the source of truth, so an occasional dump is enough (again, how-to/back-up-and-restore.md).