Mysten Incubation
Features

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.

devstack.config.ts
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'. Two postgres(...) 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 runs createdb idempotently via psql -tAc existence checks, so repeat boots are no-ops.
  • hostPort — optional host-port publication (e.g. 5432). Mutually exclusive with route: true.
  • route — when true, contribute a TCP routable on the postgres-tcp Traefik entrypoint (host port 5432 by default). Default false. Only one stack on the host may set route: true at a time — TCP entrypoints serve one backend at a time and the router collision detector fails fast on the second contribution.
  • readyTimeoutMspg_isready budget, default ample for cold-boot.
  • stopGraceSeconds — WAL-flush window on docker stop, default 20 (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:

  • PostgresPluginErrorphase: 'network-create' | 'image-build' | 'container-start' | 'ready-probe' | 'db-ensure' | 'unknown'.
  • PostgresConnectionTimeoutpg_isready did not succeed before the deadline.
  • DatabaseCreateFailedop: 'exists-check' | 'createdb'. Typical root cause: case-sensitivity collision in Postgres identifier folding.
  • PostgresConfigError — invalid options (empty databases, conflicting route + hostPort).

See Errors for the full tag catalog.

On this page