Skip to content

PostgreSQL and migrations

@rungsjs/store-postgres keeps requests, the audit trail, the outbox and the inbox in PostgreSQL 15 or newer. It has no dependencies of its own: you pass the driver you already use.

export async function connect(connectionString: string): Promise<Rungs> {
const db = fromPg(new pg.Pool({ connectionString, max: 10 })); // more than one connection
await migrate(db); // creates or upgrades the tables; safe to run from several processes
return createRungs({
store: postgresStore(db),
clock: () => new Date(),
ids: () => crypto.randomUUID(),
});
}

Runs in CI:examples/docs-snippets/src/guides/postgres.ts

Driver Adapter
node-postgres (pg) fromPg(pool)
postgres.js fromPostgresJs(sql)
PGlite (in-process, for tests and demos) fromPglite(db)

The pool needs more than one connection: callbacks such as roles run while a command holds its connection. A single-connection database such as PGlite works when callbacks do not query it.

migrate(db) applies missing migrations in one transaction under an advisory lock, so several processes can start at once. Migrations only go forward. A store refuses to work on an older schema and tells you to run migrate(), so a forgotten migration fails loudly.

Tables live in schema public with prefix rungs_ by default:

const approvals = fromPglite(await PGlite.create());
await migrate(approvals, { schema: "approvals", tablePrefix: "wf_" });
const store = postgresStore(approvals, { schema: "approvals", tablePrefix: "wf_" });

Runs in CI:examples/docs-snippets/src/guides/postgres.ts

If your migrations run through another tool, generate the SQL instead. Each migration records its version, so migrate() and the version check keep working afterwards:

// For Prisma, Drizzle, Flyway or plain psql: the same migrations as SQL.
const everything = migrationSql({ schema: "approvals" });
const upgradeFrom1 = migrationSql({ schema: "approvals", from: 1 });

Runs in CI:examples/docs-snippets/src/guides/postgres.ts

The same scripts ship in the package as migrations/0001_init.sql, 0002_list_queries.sql and so on.

Commands lock the request row and save with a compare-and-set on its version. tick and event delivery claim rows with FOR UPDATE SKIP LOCKED, so several workers can run side by side without doing the same work; correctness never depends on that lease (EC-27). The store is tested against PostgreSQL 15, 16, 17 and 18 with both node-postgres and postgres.js.