DEV Community

Mark F A
Mark F A

Posted on

PGlite Is Great. It Still Is Not Your Local Postgres Stack.

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" } ] }
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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)