Skip to content

From PGlite to Production Postgres

7 min readPostgreSQLDatabasesDeveloper Tools

There is a specific moment in a project's life where an embedded database stops being enough. Usually it arrives without warning. Someone else wants to use the thing. Or you deploy a server component and it needs to read the same rows the browser has been writing. Or you want a cron job, a webhook, a second device, a teammate. The database that has been living inside your app now needs an address.

If your prototype was built on PGlite, the good news is that this is one of the easiest graduations in the business, because you were already running Postgres. Not something Postgres-shaped. Actual Postgres, compiled to WebAssembly, running in process.

The timing is why everyone is talking about this right now. On August 11, 2026, Electric joined the Neon team inside Databricks, and the number in that post is the one worth staring at: PGlite went from 1 million to 13 million weekly downloads in a year. The thesis behind it is that coding agents are driving the cost of building software toward zero, so there is about to be an enormous number of small applications. A lot of those start embedded, because embedded is the fastest way to get something working. Some fraction of them will get real users. That fraction needs this guide.

Why PGlite graduates cleanly

Most embedded databases make you pay on the way out. You wrote against SQLite's type affinity or a document store's query language, and moving to Postgres means rewriting a schema, rewriting queries, and rediscovering every place your code assumed the old engine's behavior.

PGlite does not have that problem, and it is worth being precise about why. It is not a reimplementation of Postgres and it is not Postgres running inside an emulated Linux VM. As the project puts it, it is "simply Postgres in WASM," using Postgres' own single-user mode with a custom I/O path into JavaScript. Your CREATE TABLE statements are Postgres DDL. Your queries are Postgres SQL. Your SERIAL columns, your jsonb, your constraints and defaults all mean exactly what they mean on a server.

So the migration is a dump and a restore. That is the entire shape of it.

Step 1: dump the PGlite database

PGlite ships a companion package that runs the real pg_dump against a live PGlite instance. Install it alongside PGlite:

bash
npm install @electric-sql/pglite @electric-sql/pglite-tools

pgDump takes your PGlite instance and hands back a File you can read as text:

ts
import { PGlite } from '@electric-sql/pglite'
import { pgDump } from '@electric-sql/pglite-tools/pg_dump'
import { writeFileSync } from 'node:fs'

const pg = await PGlite.create('./pgdata')

const dump = await pgDump({ pg })
writeFileSync('pglite-dump.sql', await dump.text())

That is the whole step in Node. The dump is standard SQL: schema, then data. pgDump appends --inserts by default, so the data comes back as ordinary INSERT statements rather than COPY blocks, which makes the file restorable into any compatible PostgreSQL server. Compatible is doing some work in that sentence: check that the target's major version is not older than the one PGlite was built on, and that any extension your schema leans on exists there too.

If your data lives in the browser instead of on disk, the same call works against a browser-backed instance. Open the database at whatever data directory you have been using, idb://my-app for IndexedDB, run pgDump, and download the returned File instead of writing it:

ts
const pg = await PGlite.create('idb://my-app')
const dump = await pgDump({ pg })

const url = URL.createObjectURL(dump)
Object.assign(document.createElement('a'), {
  href: url,
  download: 'pglite-dump.sql',
}).click()

If you kept the data in OPFS at opfs-ahp://my-app, the same call works, but that filesystem only runs inside a Web Worker, and it does not work in Safari at all: a Postgres install opens more sync access handles than Safari allows. Run the dump from the worker that owns the database.

Two things worth knowing before you move on. pgDump runs DEALLOCATE ALL on the connection when it finishes, so any prepared statements you were holding are gone. And the dump does not carry your session search_path, so if you were relying on a non-default one, note what it was before you dump and set it on the other side.

Open the file. It is human-readable SQL, and reading it is a good sanity check that everything you expected made it in.

Step 2: create a hosted Postgres

Two paths, both real. Through the dashboard, pick Postgres at layerbase.com/create/postgresql, name it, and you have a database in under a minute with a connection string on the detail page.

From a terminal, the Layerbase CLI does the same thing. Cloud commands authenticate with a personal API key from your dashboard settings, which is also what makes this scriptable later:

bash
npm i -g layerbase
export LAYERBASE_API_KEY=sk_...

lbase cloud create my-app --engine postgresql
lbase cloud connection-string my-app
text
postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/my-app?sslmode=require

The free tier covers this: two databases, 5 GB, one branch each, no credit card. That is deliberate, and I will come back to why it stays that way.

Step 3: restore the dump

You have a SQL file and a connection string, so this is the standard Postgres move:

bash
psql -X -v ON_ERROR_STOP=1 "$DATABASE_URL" -f pglite-dump.sql

-X skips your .psqlrc so nothing you set up locally leaks into the restore, and ON_ERROR_STOP=1 halts on the first problem rather than ploughing through and leaving you guessing. On a fresh database there usually is not a first problem, because the dump was produced by pg_dump from a Postgres that agrees with the one receiving it. If it does stop partway, start clean: drop and recreate the database, or delete it in the dashboard and make another, rather than re-running the file over a half-loaded schema.

If you would rather not have psql installed, the CLI can push the file for you with lbase import pglite-dump.sql --target my-app --yes, and the query console in the dashboard will run statements against the database directly from the browser.

Step 4: swap the connection string

This is the step people brace for and it is anticlimactic. Wherever you had:

ts
const pg = await PGlite.create('./pgdata')

you now have a normal Postgres client pointed at an environment variable:

ts
import { Pool } from 'pg'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

Your queries do not change. If you were using an ORM, its Postgres dialect was already the dialect you were using. What changes is that the database is now a thing on the network with credentials and TLS, which is exactly the property you needed when you started this.

What you actually get on the other side

Graduating buys you the operational layer an embedded database has no way to give you.

Backups you did not have to think about. Every destructive path takes a backup first, deletion is refused if a final backup cannot be secured, and the most recent backup is never pruned. That applies on the free tier, not just paid ones.

Branching. Fork a writable copy of the database before a risky migration, run it, and throw the branch away if it goes badly. It works on nine engines, Postgres among them. Details in the branching docs.

A query console, so debugging production data does not require a psql session and a laptop.

Sleep and wake, instead of a bill. Free databases sleep after 60 idle minutes and wake when you reconnect, which takes a few seconds and looks like a slow first connection rather than an error. No keep-alive cron job, no dashboard button, no restore flow. That mechanic is the whole reason the free tier can stay: a sleeping database holds no RAM and no CPU, so we are not carrying a fleet of abandoned side projects at a loss. I wrote out the full economics in the free tier that stays.

Postgres is one of 18 engines on Layerbase Cloud, which matters mostly for the next thing you add. When the app that outgrew PGlite wants a cache or a search index, it goes on the same account instead of a new vendor.

The honest fork in the road

Not every prototype should graduate, and I would rather say that plainly than pretend otherwise.

If your app is genuinely local-first, if the data belongs to the person using it and never needs to be reachable from a server, PGlite is the right answer and you should keep it. That is the case it was built for, and it is very good at it. Same if you are using it as a test fixture or a sandbox: an in-process Postgres that starts in milliseconds is hard to beat. And if what you want is a real Postgres server on your own machine rather than a hosted one, the Layerbase CLI runs those locally too, no Docker required.

The fork is not about scale or seriousness. It is one question: does something other than this one client need to read or write these rows? The day the answer becomes yes, you have a ten-minute job and no rewrite, because you picked a database that was Postgres the whole time.

Create a Postgres database and run the dump. If you want the background on the engine itself first, the PostgreSQL page covers what we run and how, and connecting to a database covers the client side in more depth.