Postgres
Run a managed Postgres container alongside the stack and consume it from app code via generated bindings.
The postgres() plugin runs one Postgres container per (stack, name) pair, ensures the requested
databases, and emits typed connection bindings into src/generated so app code can compose
DATABASE_URL strings without hard-coding credentials.
Postgres is a topological leaf — it does not depend on Sui or any other plugin. Single mode: only the local container exists today. The plain-endpoint shape anticipates a future live mode (Cloud SQL / Neon / RDS) without a code-shape change at the call site.
import { defineDevstack, postgres, sui } from '@mysten-incubation/devstack';
const localnet = sui();
const db = postgres({
databases: ['app', 'jobs'],
});
export default defineDevstack({
members: [localnet, db],
stackName: 'main',
});Options
All fields are optional. The defaults track the distilled-doc recommendation (Postgres 17-alpine).
name— instance name, default'postgres'. Twopostgres(...)calls in one stack must use different names.version— image tag (default tracks 17-alpine).user— superuser name, default'devstack'.password— credential. If omitted, devstack derives a deterministic per-stack password from the identity(app, stack, stackRoot)so the value is stable across cold boots without leaking through config. Multiple checkouts of the same(app, stack)derive distinct passwords by design.databases— non-empty array of database names to ensure. The plugin runscreatedbidempotently viapsql -tAcexistence checks, so repeat boots are no-ops.hostPort— optional host-port publication (e.g.5432). Mutually exclusive withroute: true.route— whentrue, contribute a TCP routable on thepostgres-tcpTraefik entrypoint (host port 5432 by default). Defaultfalse. Only one stack on the host may setroute: trueat a time — TCP entrypoints serve one backend at a time and the router collision detector fails fast on the second contribution.readyTimeoutMs—pg_isreadybudget, default ample for cold-boot.stopGraceSeconds— WAL-flush window ondocker stop, default20(10s risks SIGKILL on busy databases and recovery mode on next boot).
Resolved value
postgres() resolves to a Postgres handle. Inside a plugin's start body, the resolved value is:
interface Postgres {
readonly name: string;
readonly user: string;
readonly password: string;
readonly host: string;
readonly port: number;
readonly databases: ReadonlyArray<string>;
readonly endpoint: string; // credentialed cluster URL
readonly plainEndpoint: string; // manifest-safe (no password)
readonly url: (db: string) => string;
readonly containerNetwork: string;
readonly networkAlias: string;
}Logging convention: never log the entire handle — log plainEndpoint instead. The manifest
projection strips the password.
Generated bindings
A postgres-connection codegen capability emits a typed bindings module under src/generated. App
code reads the bindings (a PostgresConnectionBindings record exposing user, password, host,
port, and databases) and composes a per-database URL with the standard Postgres URL shape:
import { postgres } from '@/generated/postgres.ts';
const { user, password, host, port } = postgres;
const url = `postgres://${user}:${password}@${host}:${port}/app`;The codegen host is the per-stack network alias Docker registers via --network-alias. Dialing
the alias resolves in-container regardless of which parallel stack the caller belongs to.
Snapshot integrity
The vendored image sets ENV PGDATA=/var/lib/postgresql/data-devstack so writes land in the
container's writable layer rather than the upstream VOLUME /var/lib/postgresql/data path (which
docker commit excludes). This is the load-bearing invariant for snapshot integrity — restoring a
captured Postgres image must roundtrip the database contents.
Failure surface
Postgres-plugin callers may encounter:
PostgresPluginError—phase: 'network-create' | 'image-build' | 'container-start' | 'ready-probe' | 'db-ensure' | 'unknown'.PostgresConnectionTimeout—pg_isreadydid not succeed before the deadline.DatabaseCreateFailed—op: 'exists-check' | 'createdb'. Typical root cause: case-sensitivity collision in Postgres identifier folding.PostgresConfigError— invalid options (emptydatabases, conflictingroute+hostPort).
See Errors for the full tag catalog.