Installation
One pool, four providers, and a schema you own.
Install
pnpm add @sigx/actors-pg pgPostgres ≥ 13, pg ≥ 8.
Create the schema
Tables live in one Postgres schema, sigx by default. DDL is explicit — nothing issues it
implicitly, so a production role that never runs the bootstrap needs only DML grants.
import pg from 'pg';
import { ensurePgSchema, pgSchemaSql } from '@sigx/actors-pg';
const pool = new pg.Pool({ connectionString: process.env.PG_URL });
await ensurePgSchema(pool);
Every replica may call this at boot, concurrently. ensurePgSchema() takes
pg_advisory_xact_lock as the first statement of the same multi-statement string as the DDL,
so the lock spans the whole thing and releases at commit — before the pooled client is
recycled. A bounded, jittered retry sits underneath as a backstop.
Two things to know about it:
pgSchemaSql()takes no lock. It is pure DDL, for a migration tool that brings its own serialization.ensurePgSchema()expects to own its transaction. Called with aPgQueryablealready inside your open transaction, a failure poisons that transaction and the retry burns its attempts on25P02.
In production, running the SQL through your migration tool remains the better shape:
console.log(pgSchemaSql('sigx'));
Wire it up
import { defineActorApp } from '@sigx/actors/host';
import { cluster } from '@sigx/actors/cluster';
import { pgCluster, pgReminders, pgStorage } from '@sigx/actors-pg';
export const app = defineActorApp({
actors,
storage: pgStorage({ pool }),
reminders: pgReminders({ pool }),
}).use(cluster({
providers: pgCluster({ pool }),
advertise: `http://${process.env.POD_IP}:7311`,
secret: process.env.HOST_SECRET,
}));
Every provider takes pool or url — with url, the package constructs its own
pg.Pool. Sharing one pool is the point of this package.
Options
| Option | Default | Applies to | Meaning |
|---|---|---|---|
pool / url | — | all | a pg.Pool, or a URL to construct one |
schema | sigx | all | validated as an SQL identifier |
heartbeatMs | 5000 | membership | heartbeat cadence |
ttlMs | 15000 | membership | expiry, on the database clock |
pollMs | 5000 | membership | view poll cadence, and the propagation bound |
Pool sizing
Every provider shares the pool, and pgMembership() checks out a connection to hold LISTEN
when it can. Size the pool with that in mind — a pool of one works, but pushes membership onto
the poll path permanently.
Verify
PG_URL=postgres://postgres:postgres@localhost:5432/postgres pnpm test -- actors-pg
The provider suite is env-gated on PG_URL; CI provides a postgres:16 service container and
the rest of the matrix skips cleanly.
Next steps
- API reference — exports, tables and semantics.
- Timers & reminders — what
pgReminders()replaces. - Storage — the seam
pgStorage()implements.
