Skip to content
devsess

devsess/pglite

Per-session PGlite database seeded from your Drizzle migrations — each session gets its own Postgres-compatible database with no server running. See A database per session.

Install

Optional peers — only needed if you use this entry point.

bun add @electric-sql/pglite drizzle-orm
PeerVersion
@electric-sql/pglite>=0.2.0
drizzle-orm>=0.30.0

PGlite below refers to @electric-sql/pglite's client type. PgliteMigrations below refers to { migrationsFolder: string, migrationsTable?: string, migrationsSchema?: string }migrationsTable/migrationsSchema default to Drizzle's own defaults (__drizzle_migrations in the drizzle schema) and only need setting if your drizzle.config.ts's migrations.table/migrations.schema does.

prepareSessionPglite requires the Effect DevSession (from devsess), not the devsess/async one — its path() must return an Effect, and the async DevSession.path() returns a plain string instead. The bridge back to the Effect session isn't exported from devsess/async, so there's no supported way to call prepareSessionPglite with a session obtained there. The rest of this entry point (openLitePglite and the lower-level helpers) takes plain values instead of a DevSession, but every export still returns an Effectdevsess/async doesn't export a way to run one, so using this entry point from an async dev script means importing effect yourself.

prepareSessionPglite

function prepareSessionPglite(
	session: DevSession,
	opts: PgliteMigrations
): Effect.Effect<
	{ client: PGlite, dataDir: string, dumpPath: string },
	PgliteError | PlatformError,
	FileSystem | Path
>

Session-scoped entry point. Resolves dataDir to <session>/pglite and dumpPath to <session>/pglite.dump, then delegates to openLitePglite. Returns the open client plus the resolved paths — e.g. pass dataDir to the app as DATABASE_LITE_PATH.

Because the dump lives inside the session directory, it isn't shared across sessions — a brand-new session pays the full migration-replay cost again.

openLitePglite

function openLitePglite(opts: PgliteMigrations & {
	dataDir: string
	dumpPath: string
}): Effect.Effect<PGlite, PgliteError | PlatformError, FileSystem | Path>

Same ensure-dump → hydrate → migrate flow as prepareSessionPglite, against paths you supply directly — for use outside dev sessions (tests, scripts).

  1. ensurePgliteDump — builds the dump only if dumpPath is missing.
  2. createPgliteFromDump — if dataDir already exists on disk, opens it directly and the dump is never read. Otherwise hydrates a new client from the dump.
  3. Staleness guard — if migrations are expected (getExpectedMigrationCount > 0) but the opened database has no Drizzle migration bookkeeping at all (getDbMigrationCount is 0), fails with a PgliteError naming the rm -rf <dataDir> <dumpPath> recovery command. getDbMigrationCount looks in the same migrationsTable/migrationsSchema you passed in — if your drizzle.config.ts sets a custom migrations.table/migrations.schema and you don't pass the matching override here, this guard checks the wrong (empty) table and fails against a database that's actually fully migrated.
  4. migratePglite always runs afterward — Drizzle's migrate() is idempotent, so this only applies migrations added since the dump/dataDir was built.

Restarting within an existing session costs nothing: dataDir already exists, so the dump is never read. The dump only earns its keep the first time a session opens a database with no dataDir yet.

PgliteError

class PgliteError extends Data.TaggedError('PgliteError')<{
	message: string
	cause?: unknown
}> {}

Every devsess/pglite export fails with this tagged error (_tag: 'PgliteError'). cause is optional — set when the error wraps another rejected call (a PGlite or Drizzle failure).

Lower-level helpers

prepareSessionPglite and openLitePglite cover most use cases. These are their building blocks.

ExportSignatureDescription
ensurePgliteDump(opts: PgliteMigrations & { dumpPath: string }) => Effect<void, PgliteError | PlatformError, FileSystem | Path>Builds the dump via buildPgliteDump, only if dumpPath doesn't exist yet
buildPgliteDump(opts: PgliteMigrations & { dumpPath: string }) => Effect<void, PgliteError | PlatformError, FileSystem | Path>Runs every migration on a fresh in-memory PGlite (memory://) and writes the result to dumpPath
createPgliteFromDump(opts: { dataDir?: string, dumpPath: string }) => Effect<PGlite, PgliteError | PlatformError, FileSystem>Opens dataDir directly if it's set, isn't 'memory://', and already exists; otherwise hydrates a client from dumpPath (fails if that's also missing)
dumpPgliteToFile(client: PGlite, dest: string) => Effect<void, PgliteError | PlatformError, FileSystem | Path>Serializes an open client's data dir to dest, creating parent directories as needed
migratePglite(client: PGlite, migrations: PgliteMigrations) => Effect<void, PgliteError, never>Runs Drizzle's migrate() against the client
getDbMigrationCount(client: PGlite, migrations?: Pick<PgliteMigrations, 'migrationsTable' | 'migrationsSchema'>) => Effect<number, PgliteError, never>Counts rows in the migrations table — drizzle.__drizzle_migrations by default, or migrations.migrationsSchema/migrations.migrationsTable if passed; 0 if the table doesn't exist yet
getExpectedMigrationCount(migrationsFolder: string) => Effect<number, PgliteError | PlatformError, FileSystem | Path>Counts entries in <migrationsFolder>/meta/_journal.json; fails if the journal is missing, unparseable, or has no entries array