serverpod_embedded_postgres
Run a real PostgreSQL server as a child process for Serverpod local development. Same PG dialect as production, no Docker dependency, no TCP port conflicts by default. One Dart call boots the cluster, persistent across restarts.
Quickstart
import 'dart:io';
import 'package:postgres/postgres.dart';
import 'package:serverpod_embedded_postgres/serverpod_embedded_postgres.dart';
Future<void> main() async {
final pg = await EmbeddedPostgres.start(
EmbeddedPostgresOptions(
dataDir: Directory('.serverpod/pgdata'),
databaseName: 'projectname',
),
);
final conn = await Connection.open(pg.endpoint);
final rs = await conn.execute('SELECT 1');
print(rs.first.first); // 1
await conn.close();
await pg.stop();
}
The first call downloads ~30 MB of Zonky's
embedded-postgres-binaries from Maven Central into the
per-user cache (~/Library/Caches/serverpod/pg-binaries on macOS,
$XDG_CACHE_HOME/serverpod/pg-binaries on Linux,
%LOCALAPPDATA%\serverpod\Cache\pg-binaries on Windows). Subsequent
starts reuse the cache and reach ready in under a second on a warm
cluster.
Two transports
Unix Domain Socket (default). Trust authentication; the project
directory already gates filesystem access to the socket. PG chdirs to
PGDATA before binding so unix_socket_directories = '../run' lands a
~20-byte path in sockaddr_un.sun_path, well under the 104-byte macOS
cap regardless of how deep your project lives.
TCP loopback (TcpTransport). scram-sha-256 against 127.0.0.1,
random 32-char password persisted to <.serverpod>/postgres.password
for warm-restart consistency. TcpTransport(port: 0) (the default) gets
an ephemeral port; explicit ports are honored. Port-race collision is
retried up to 3 times before bubbling up.
// TCP variant:
final pg = await EmbeddedPostgres.start(
EmbeddedPostgresOptions(
dataDir: Directory('.serverpod/pgdata'),
databaseName: 'projectname',
transport: const TcpTransport(),
),
);
print(pg.endpoint.port); // some ephemeral port like 49152
print(pg.endpoint.password); // random 32-char string
Detach + attach for cross-VM dev DBs
Set detach: true to keep the postmaster alive after your Dart VM
exits, then reconnect from a fresh process:
// First VM (e.g. `serverpod start --watch`):
await EmbeddedPostgres.start(
EmbeddedPostgresOptions(
dataDir: Directory('.serverpod/pgdata'),
databaseName: 'projectname',
detach: true,
),
);
// Second VM (e.g. `dart test`):
final pg = await EmbeddedPostgres.attach(Directory('.serverpod/pgdata'));
final conn = await Connection.open(pg.endpoint);
// ...
attach() reads the pidfile and embedded_postgres_state.json left by
the original start(), verifies the recorded PID is still our
postmaster (cmdline + cwd, NOT just PID - the OS recycles those), and
hands back a fully usable handle. Stale pidfiles (process gone) are
cleaned up; PID-recycled foreign processes are left strictly alone.
Prefetch for CI
Pre-populate the cache without booting a postmaster:
dart run serverpod_embedded_postgres:prefetch
dart run serverpod_embedded_postgres:prefetch --version 16.13.0
dart run serverpod_embedded_postgres:prefetch --target linux-amd64
--target lets a CI runner warm the cache for non-host platforms, e.g.
a macOS shared-cache job pre-extracting the linux-amd64 bundle for
test workers to consume.
What's exposed
abstract class EmbeddedPostgres {
// Boot or reattach.
static Future<EmbeddedPostgres> start(EmbeddedPostgresOptions opts);
static Future<EmbeddedPostgres> attach(Directory dataDir);
// Cache utilities.
static Future<void> prefetch(Version version, {String? target});
static Directory defaultBinaryCache();
// Connection handles.
pg.Endpoint get endpoint; // package:postgres consumers
String get connectionString; // libpq URI for psql, pg_dump, etc.
Uri get connectionUri;
// Lifecycle.
Version get version;
int? get pid;
bool get isRunning;
Future<void> stop({Duration timeout = const Duration(seconds: 10)});
Future<void> reset(); // wipe data dir, ready for fresh init
}
The Transport sealed class has two variants:
sealed class Transport { const Transport(); }
final class UnixTransport extends Transport { const UnixTransport(); }
final class TcpTransport extends Transport {
final int port; // 0 = ephemeral
final String? password; // null = generate random
const TcpTransport({this.port = 0, this.password});
}
Errors are a sealed hierarchy rooted at EmbeddedPostgresException:
BinaryFetchException, BinaryVerificationException,
UnsupportedPlatformException, InitdbException,
StartupTimeoutException, CrashedException (carries logTail),
AttachException, StaleClusterException. switch over them
exhaustively.
What's not included
- PostgreSQL extensions beyond what Zonky's stock binaries ship.
Notably, the default Serverpod project template uses
pgvector/pgvector:pg16, so this package isn't yet a drop-in for newly-created projects until pgvector artifacts ship - tracked separately for a follow-up release. - Replication / logical decoding / hot standby - out of scope for a dev-loop tool.
- pg_dump / pgAdmin wrappers - use
pg_dumpdirectly againstconnectionString. - Encryption at rest - dev tool.
- Connection pooling - delegated to
package:postgres.
Design
See docs/design/serverpod_embedded_postgres_spec.md for the full
design, including the rationale for UDS-by-default, binary-source
choice, supervisor lifecycle, error model, and verification plan.
Platform support
See PLATFORMS.md for which (OS, arch) tuples have
been verified end-to-end and where Windows currently degrades.
Libraries
- serverpod_embedded_postgres
- Run a real PostgreSQL server as a child process for Serverpod local dev.