TL;DR
- PGlite is real Postgres compiled to WASM: no Docker, no install, starts in milliseconds
- It is brilliant for unit tests, CI, and embedded/in-browser databases
- It is single connection, speaks no wire protocol out of the box, and ships none of the platform layer (auth, storage, auto API) your app talks to
- Use PGlite for tests. Use a real Postgres server (there are single-binary options now) when your app expects a database plus the services around it
What PGlite actually is
PGlite is Postgres compiled to WebAssembly, wrapped in a TypeScript client. Not a Linux VM running Postgres, not a lookalike engine: the actual Postgres query planner and executor, running inside your JS runtime.
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite();
await db.query("select 'hello' as message;");
// { rows: [ { message: "hello" } ] }
That is a working Postgres in two lines. In memory by default, persistable to the filesystem in Node or IndexedDB in the browser, roughly 3 MB gzipped, with extension support including pgvector.
Where it genuinely wins
Unit tests and CI. This is the killer use case. A fresh, isolated Postgres per test, created and destroyed in milliseconds:
import { PGlite } from "@electric-sql/pglite";
test("inserts a user", async () => {
const db = new PGlite(); // brand new database, every test
await db.exec("create table users (id serial, email text)");
await db.query("insert into users (email) values ($1)", ["a@b.co"]);
const res = await db.query("select count(*)::int as n from users");
expect(res.rows[0].n).toBe(1);
});
No shared test database, no truncate-between-tests hacks, no container spin-up in CI.
Embedded and in-browser databases. Local-first apps, demos, playgrounds: shipping Postgres inside the page is a legitimately new capability.
Where it stops being your dev database
One connection. Ever. PGlite runs Postgres in single-user mode because WASM cannot fork processes. Your app opens a pool of five connections? Not happening. Two services sharing one dev database? Not happening.
No wire protocol by default. psql, TablePlus, your ORM's normal driver: all of them speak the Postgres protocol over TCP. PGlite is a library call, not a server. There is a socket proxy add-on that bridges this, but at that point you are assembling infrastructure, which was the thing you were avoiding.
No platform layer. If your app is built against a Supabase-style stack, the database is maybe a third of what it talks to. Auth endpoints, row level security backed by real roles, storage buckets, the auto-generated REST API: none of that exists in a bare WASM Postgres. Your app boots, then dies on the first supabase.auth call.
This is the gap single-binary runtimes are filling. Tinbase, for example, is an MIT-licensed single binary that runs a real Postgres server with a Supabase-compatible surface on top: real connections, real wire protocol, no Docker. Same "just run it" feel PGlite gives you, but for the whole stack instead of the engine alone.
The decision table
| You need | Reach for |
|---|---|
| Fresh DB per unit test | PGlite |
| Postgres in the browser | PGlite |
| psql / GUI clients / connection pools | Real Postgres server |
| Supabase-style auth, storage, REST | Supabase-compatible runtime |
| CI without containers | Either, depending on the two rows above |
The honest framing: PGlite is not a worse local Postgres. It is a different shape of Postgres, and the shape matters more than the engine.
Closing
I have moved all my unit tests to PGlite and my app-level dev environment to a single-binary runtime, and the two do not compete at all.
What is your local Postgres setup right now: Docker, WASM, single binary, or "I just point dev at a cloud database and hope"? Drop it in the comments, especially if it is the last one.
Top comments (0)