Postgres has a reputation as a server-shaped database: install the daemon, create users, manage a service. But more and more tools now run Postgres inside or alongside your application, started and stopped with it, the way SQLite always has.
This post covers what "embedded Postgres" means, three practical ways to do it in JavaScript, the trade-offs of each, and when you should still reach for a regular Postgres server.
What does "embedded Postgres" mean?
People use the term for two different things:
- In-process: Postgres runs inside your application's process, like SQLite. Historically impossible for Postgres; PGlite makes it possible by compiling Postgres to WebAssembly.
- App-managed: Postgres runs as a separate child process, but your app downloads, starts, and stops it. From the developer's point of view it feels embedded, even though it's a real server process.
Both are useful. They just have different trade-offs.
Option 1: PGlite (in-process, WebAssembly)
PGlite is Postgres compiled to WebAssembly. It runs in Node, Bun, Deno, and the browser, with no separate process.
npm install @electric-sql/pglite
import { PGlite } from '@electric-sql/pglite';
const db = new PGlite('./pgdata'); // or omit the path for in-memory
await db.exec('CREATE TABLE items (id SERIAL PRIMARY KEY, name TEXT)');
await db.query('INSERT INTO items (name) VALUES ($1)', ['first']);
const { rows } = await db.query('SELECT * FROM items');
Good at:
- Running anywhere JavaScript runs, including the browser (with IndexedDB persistence)
- Disposable databases for tests, with no ports or cleanup
- Local-first and offline apps
Trade-offs:
- Single connection. One query runs at a time.
- Extensions are limited to those compiled for WASM.
- Memory can be higher than you'd expect for a "lightweight" option, because the WASM runtime reserves memory up front.
Option 2: embedded-postgres (app-managed native Postgres)
The embedded-postgres package downloads native Postgres binaries and runs a real Postgres server as a child process of your Node app.
npm install embedded-postgres
import EmbeddedPostgres from 'embedded-postgres';
const pg = new EmbeddedPostgres({
databaseDir: './data',
user: 'postgres',
password: 'password',
port: 5433,
persistent: true,
});
await pg.initialise(); // first run only
await pg.start();
// connect with any Postgres client (pg, postgres.js, Drizzle, Prisma...)
await pg.stop();
Good at:
- Full native Postgres behavior, many connections, and native extensions
- Working with any ORM or driver, since it's a normal Postgres server
Trade-offs:
- It's just the database. Auth, APIs, and storage are up to you.
- It needs a free port, and you manage the process lifecycle.
Option 3: tinbase (Supabase-compatible backend around Postgres)
Full disclosure: this is the one I work on. tinbase wraps Postgres in a Supabase-compatible backend: REST API, auth, storage, realtime, and edge functions, so the official supabase-js client works against it unchanged.
npx tinbase start
It reads your supabase/migrations/ and seed.sql, and serves the API at http://127.0.0.1:54321. Under the hood, it can use different engines:
| Engine | What it is | Default on |
|---|---|---|
native |
Embedded native Postgres 17 (binaries downloaded once, about 12 MB) | macOS, Linux |
wasm |
PGlite | Windows |
pgmem |
In-memory JavaScript engine, for quick previews | — |
You can also run it in-process from Node or the browser with createBackend, and build it into a single executable.
Good at:
- Local development and CI for Supabase apps, without Docker
- Testing RLS policies and
supabase-jscode against a real backend
Trade-offs, stated plainly:
- It's alpha, and built for development, testing, and prototypes, not production.
- It's a reimplementation, so it has documented gaps: no MFA, SSO, or phone auth; no resumable uploads or image transforms; some PostgREST features like aggregates aren't implemented yet.
- One writer at a time.
How do they compare?
| PGlite | embedded-postgres | tinbase | |
|---|---|---|---|
| Runs | In-process (WASM) | Child process (native) | Native, WASM, or in-memory |
| Browser support | Yes | No | Yes (WASM engine) |
| Concurrent connections | One | Many | Serialized |
| API layer | None (SQL) | None (SQL) | Supabase-compatible |
| Best for | Tests, browser, local-first | Local dev with any ORM | Local dev and CI for Supabase apps |
What about performance numbers?
Benchmarks for these tools depend heavily on the machine, the workload, and the engine, so I'd rather not publish a single table that looks more precise than it is. Here's what tinbase's own README reports, measured on Apple Silicon for the whole backend (not just the database):
| Setup | Memory at boot |
|---|---|
| tinbase, native engine | ~59 MB |
| tinbase, WASM (PGlite) engine | ~610 MB |
| Supabase local stack (Docker) | ~1,441 MB |
The gap between native and WASM is the useful lesson: in-process isn't automatically lighter. Run your own workload on your own machine before deciding.
Which one should you use?
- Fresh databases in unit tests: PGlite
- A browser demo or local-first prototype: PGlite (or tinbase's WASM engine if you need the Supabase API)
- Local dev with Prisma, Drizzle, or a plain driver: embedded-postgres
- Local dev and CI for a Supabase app: tinbase
- Production: none of these. Use managed Postgres or a properly operated server.
How do you back up an embedded database?
For a quick copy, stop the database first, then archive the data directory. Copying the files of a running Postgres can give you a corrupted backup.
tar -czf backup-$(date +%Y%m%d).tgz ./data
For a consistent snapshot while it's running, use pg_dump against tools that expose a real Postgres connection, such as embedded-postgres:
pg_dump -h 127.0.0.1 -p 5433 -U postgres postgres > backup.sql
For PGlite, use its dump/export APIs (see the PGlite docs). For Supabase-style projects, the most important backup is already in your repo: your migrations and seed files can rebuild the database from scratch.
What about major version upgrades?
Postgres major versions change the on-disk format, so a data directory from one major version can't simply be opened by the next. Postgres 18 shipped in 2025, and embedded tools adopt new majors on their own schedules.
For development databases, the easiest path is usually to recreate from migrations and seed data rather than upgrade in place. For anything you can't recreate, plan a pg_dump and restore across the version change.
When is embedded the wrong choice?
- Multiple app servers sharing one database: use a Postgres server.
- Heavy concurrent writes: single-connection engines will serialize them.
- High availability, replicas, or failover: use managed Postgres.
- Anything production-critical: embedded tools are for development, tests, demos, and local-first apps.
Wrapping up
Embedded Postgres has gone from impossible to genuinely useful. PGlite brings real Postgres into the process and the browser, embedded-postgres makes a native server feel like a dependency, and Supabase-compatible tools like tinbase add the API layer on top for local development.
Pick by what you need around the database, measure on your own workload, and keep production on a real server.
What's your setup for local Postgres in 2026? Share it in the comments.
Top comments (0)