Setting up a database is where a lot of new projects lose their first afternoon. Install Postgres, or start a container, create a user, create a database, find the connection string, discover the port is taken. None of it is hard and all of it is friction, especially when the goal is to try an idea before lunch.
On our recent products we skipped that step entirely. In development the app talks to Postgres compiled to WebAssembly, running inside the same Node process. In production it talks to serverless Postgres. The application code is the same in both.
What PGlite is
PGlite is the real Postgres engine built as a WebAssembly module. It is not an emulator and not a different database with a similar dialect. It runs in Node or in a browser, stores its data in a folder or in memory, and needs no server process.
That distinction matters. The usual shortcut for local development is SQLite, and it works until you write a query that depends on Postgres behavior: a jsonb operator, returning with a particular shape, an on conflict clause, a timestamp with a time zone. Then the code that worked on your laptop fails in production. With PGlite the engine is the same, so that class of surprise goes away.
The pattern
We put one small module between the application and the database. It exposes a single query function and decides at startup which engine sits behind it. A simplified version:
// db.js
let query;
if (process.env.DATABASE_URL) {
const { neon } = await import('@neondatabase/serverless');
const sql = neon(process.env.DATABASE_URL);
query = (text, params = []) => sql.query(text, params);
} else {
const { PGlite } = await import('@electric-sql/pglite');
const db = new PGlite('./.data/pg');
query = async (text, params = []) => (await db.query(text, params)).rows;
}
export { query };
If a connection string is present, use the hosted database. If not, start PGlite and keep its files in a local folder. Nothing else in the codebase knows which one it got.
The dynamic imports mean production never loads the WebAssembly build and development never needs the hosted driver's network access.
Migrations run the same way
Schema changes are plain SQL files applied in order, with a table recording which have run. Because both engines are Postgres, the same files run against both. A new developer clones the repository, starts the app, and the migrations build a fresh local database on first boot.
That is the whole onboarding. There is no setup document for the database because there is no setup.
What it is good for
- Starting fast. A new project has a working database the moment the dependencies install.
- Tests. An in-memory instance starts in well under a second, so each test file can have its own clean database with the real engine behind it.
- Demos and previews. A demo mode can run on an in-memory database seeded at startup, with no shared state to reset.
- Working offline. A flight is no longer a reason to stop.
Where it is not the answer
We are specific about the limits, because the pattern is easy to oversell.
- It is a single connection. PGlite runs in one process. It is not a server and will not handle concurrent clients the way a real deployment does. Anything involving connection pools or lock contention has to be tested against the real thing.
- Not every extension exists. Common ones are available, but check before depending on one.
- Performance is not representative. It is fast enough to develop against. Do not tune queries on it and expect the numbers to carry over.
- Production is still production. Before a release, run the migrations and the test suite against a real hosted branch of the database.
The last point is the rule that keeps the convenience from becoming a liability. The local engine is for speed while building. The hosted one is the source of truth.
Why this suits serverless hosting
Serverless platforms start and stop your code constantly, so a traditional pool of open database connections does not fit. Serverless Postgres providers solve that with a driver that queries over HTTP. The query function above hides that difference as well: the application calls query and does not care whether the answer came over HTTP from another continent or from a WebAssembly module in the same process.
It also means a product can go from an empty folder to a deployed application with a production database in a day. One of ours did.
The broader habit
We like this pattern because it removes a step rather than automating it. Most tooling adds something to manage. This takes something away, and the saving repeats for every developer and every new project.
If you have a product idea that has been waiting on someone to set up the infrastructure, the infrastructure is no longer the hard part. Tell us what you want to build.