In-process PGlite bridge for Prisma. Replaces the TCP socket in
pg.Client with a Duplex stream that speaks PostgreSQL wire protocol
directly to PGlite's WASM engine.
Everything else is your production stack: the real pg client and
the official @prisma/adapter-pg run unchanged, so tests exercise
the identical adapter, wire-protocol serialization, and type-parsing
code path they ship with — only the transport differs.
Requires Prisma 7+ and Node.js 22+.
pnpm add -D prisma-pglite-bridgepnpm and npm auto-install the peer dependencies —
@electric-sql/pglite, @prisma/adapter-pg, and pg. In a
Prisma-on-Postgres project they are already your production stack;
the bridge deliberately reuses your copies rather than bundling
its own.
On Yarn (which does not auto-install peers), or to keep the peers
visible in your package.json so upgrades stay deliberate and
bot-managed, list them explicitly:
pnpm add -D prisma-pglite-bridge @electric-sql/pglite @prisma/adapter-pg pgTypeScript works out of the box — @prisma/adapter-pg ships pg's
type declarations. Add @types/pg only if your own code imports
pg directly.
Using the bridge without Prisma (drizzle, kysely, knex, typeorm,
mikro-orm)? Import from prisma-pglite-bridge/pool — that entry never
loads @prisma/* code at runtime. The Prisma packages still land in
node_modules (the peers above plus a ~5.5 MB schema engine) until
2.0; they are install weight only on this path.
// tests/users.test.ts
import { PrismaClient } from '@prisma/client';
import { createBridgeTest } from 'prisma-pglite-bridge/vitest';
import { expect } from 'vitest';
const test = createBridgeTest({
client: (adapter) => new PrismaClient({ adapter }),
migrations: true, // applies prisma/migrations, resolved via prisma.config.ts
seed: async (prisma) => {
await prisma.user.create({ data: { email: '[email protected]', name: 'Ada' } });
},
});
test('starts from the seeded snapshot', async ({ prisma }) => {
expect(await prisma.user.count()).toBe(1);
});One call boots an in-memory PostgreSQL, applies your migrations, runs
the seed once, snapshots the result, and hands every test a prisma
client restored to that snapshot. No Docker, no database server —
works in GitHub Actions, GitLab CI, and anywhere Node.js runs.
migrations: truereads the migrations directory fromprisma.config.ts(needs the optional@prisma/configpeer). Passmigrations: { migrationsPath: './prisma/migrations' }to skip config resolution, orschema: { schema }with the contents ofschema.prismawhen there is no migrations directory yet (pushSchema).- Using the
prisma-clientgenerator? ImportPrismaClientfrom your generatoroutputpath (for example./src/generated/prisma/client.js) instead of@prisma/client. - Jest:
setupPGliteBridgefromprisma-pglite-bridge/jesttakes the same options (cookbook). - Other runners (node:test, ava) or a vitest
globalSetup:createBridgeContext,createBridgeTemplate+loadBridgeTemplatefromprisma-pglite-bridge/testing— build once, load per test, cache as a file (cookbook). createBridgeTestneeds vitest ≥ 3.2 (fixture scopes). Every option, thescopedial ('file'/'worker'/'test'), and the fixtures are in the API reference.
The helper composes four building blocks you can call directly — for other test runners, a custom lifecycle, or a long-lived dev database:
import { PGliteBridge, pushMigrations } from 'prisma-pglite-bridge';
import { PrismaClient } from '@prisma/client';
import seed from './seed.ts'; // your own (prisma: PrismaClient) => Promise<void>
const bridge = new PGliteBridge();
await pushMigrations(bridge.pglite, { migrationsPath: './prisma/migrations' });
const prisma = new PrismaClient({ adapter: bridge.adapter });
await seed(prisma);
await bridge.snapshotDb(); // capture the seeded state once
beforeEach(() => bridge.resetDb()); // restore it before every test
afterAll(() => bridge.close());Skip the snapshot/reset pair if your tests are read-only or you want
state to carry over. Run prisma migrate dev first to generate
migration files; for projects without a prisma/migrations directory
see Populating the database.
Running the Prisma CLI against this bridge (shadow DB for
migrate dev, psql, SQL GUIs), or an app in a separate process
(end-to-end tests)? See PGliteServer for the
TCP/Unix-socket front and the
end-to-end recipe.
The vitest / jest helpers capture an on-failure query trail. A
failing test that shows only an assertion diff now also prints the SQL
it ran — every query, its params, its error — scoped to exactly that
test, to stderr, on by default, zero config:
pglite-bridge query trail — 3 queries — test "creates a user with a unique email"
#0 c0 0.28ms BEGIN · BEGIN
#1 c0 0.67ms QUERY · INSERT INTO users (email, name) VALUES ($1, $2) · [[email protected], Grace]
#2 c0 1.58ms QUERY · INSERT INTO users (email, name) VALUES ($1, $2) · [[email protected], Ada II]
↳ error 23505: duplicate key value violates unique constraint "users_email_key"
Each line is one query: sequence number, client tag, duration, kind, SQL, params; the failing query carries its error inline. The trail is in-memory, printed only for the failing test, and never written to disk — nothing to gitignore. Passing tests print nothing.
Params are previews of your test-fixture data, printed to the same
console your assertion failures already use. Where CI logs are durable
and shared, pass queryTrail: { redactParams: true } to render every
param as <redacted>, or set PGLITE_BRIDGE_QUERY_TRAIL=0 to turn
capture off entirely. Structured access (Jest, standalone, agents) and
the JSONL format are in the
cookbook; options and
precedence are in the API reference.
On reads, the wire-protocol path is roughly 2x faster than going
through a direct PGlite driver adapter — on every latency percentile
and every query shape — and ahead of a native local PostgreSQL
server on both Apple Silicon and x86 with the 1.7 defaults
(prepared-statement caching plus the fast query path; opt out with
preparedStatements: false). Prisma findMany({ take: 100 }), 1000
iterations, PGlite 0.5.3 (reproduced on 0.5.4), measured 2026-07-06:
| Machine | bridge (p50 / p99) | direct adapter (p50 / p99) | native Postgres (p50 / p99) |
|---|---|---|---|
| Apple M3 Max | 0.33ms / 0.54ms | 0.88ms / 1.90ms | 0.40ms / 1.05ms |
| Apple i9-9980HK | 0.87ms / 1.73ms | 2.45ms / 4.95ms | 1.03ms / 2.80ms |
| Linux i7-8700 | 0.67ms / 1.01ms | 1.69ms / 4.10ms | 0.95ms / 2.76ms |
That lead isn't an artifact of one hot query: across a nine-shape
read mix (point lookups, filters, sorts, joins, count/groupBy)
the bridge stays ahead on every shape and machine, and it now wins
indexed interactive transactions on every machine too. The trade-offs
are honest, though — give native Postgres prepared statements and the
read median becomes a near-tie (native wins it outright on one of the
three machines and keeps the tightest worst case everywhere), and
native's faster sequential scan still takes unindexed sorts on Apple
Silicon. Full tables (operation breadth, multi-shape
reads, transactions, memory, cold start, where native wins) and
methodology live in the
benchmark suite. Reproduce with:
NODE_OPTIONS="--expose-gc" pnpm bench --scenario findmany-focused -n 1000 -w 100 -r 5The same wire path serves other ORMs: PgBridgePool is a pg.Pool,
so drizzle, kysely, knex, typeorm, and mikro-orm run their standard
Postgres dialects on PGlite through prisma-pglite-bridge/pool — a
Prisma-free entry — and beat their native PGlite drivers on every
measured operation (query-builder p50 2.2–3.9×, typeorm 1.7–2.5×,
mikro-orm 1.4–1.9×; spreads in the
ORM tables). The mechanism is the same one
behind the Prisma numbers: PGlite's public query() API takes a mutex
and makes ~6 WASM protocol crossings per call, while the bridge issues
one buffered raw-stream write. Wiring recipes and driver-agnostic
testing helpers (prisma-pglite-bridge/pool/vitest, /pool/jest, and
the runner-agnostic /pool/testing builders) live in the
cookbook.
Test-setup cost matters as much as query speed for a suite.
createBridgeTest's scope trades isolation against per-test
overhead: 'file' / 'worker' reset one shared snapshot (tens of
ms/test), while 'test' hands every test its own independent,
test.concurrent-safe PGlite loaded from a per-file template — ~5x
cheaper than a full cold start per test and far more predictable
(sub-second p99 on Apple Silicon and x86, vs a cold-start tail that
runs to seconds). Numbers and the pnpm bench:isolation entry point
live in per-test isolation
cost.
- API reference — exports, options, return values, fs-sync policy.
- Cookbook — one-call Vitest / Jest
setup (
prisma-pglite-bridge/vitest,prisma-pglite-bridge/jest), per-test isolation, seed sharing, raw SQL next to Prisma, end-to-end viaPGliteServer, running in CI, extensions, persistent dev database, clean shutdown. PGliteServer— TCP / Unix-socket front for PGlite. Use for the Prisma CLI shadow database,psql, and SQL GUIs.- Stats and diagnostics —
stats()snapshots andnode:diagnostics_channelevent streams. - Troubleshooting & limitations —
known issues (PGlite version mismatch, WASM
ExperimentalWarning) and runtime constraints.
The package ships its docs: node_modules/prisma-pglite-bridge/docs/
holds the API reference, cookbook, troubleshooting guide, PGliteServer,
stats, and compatibility docs, and node_modules/prisma-pglite-bridge/AGENTS.md
is a one-page map of entry points, recipes, error codes, and foot-guns
written for coding agents. Point an agent at AGENTS.md first.
MIT