Coming from PGlite
pgwasm began as a fork of PGlite and keeps its query model: the
same query, exec, sql and transaction calls, the same result shape, live queries and a Drizzle
driver. This page is for an app that uses PGlite today without PGlite extensions and is weighing a move to
@pgxsinkit/pgwasm and @pgxsinkit/pgwasm-c. pgwasm works on its own; the rest of pgxsinkit is not needed.
bun add @pgxsinkit/pgwasm @pgxsinkit/pgwasm-cAn app that builds with Vite keeps PGlite out of dependency pre-bundling. Name both packages there instead:
export default defineConfig({ optimizeDeps: { exclude: ["@pgxsinkit/pgwasm", "@pgxsinkit/pgwasm-c"] },});Before and after
Section titled “Before and after”With PGlite:
import { PGlite } from "@electric-sql/pglite";import { live } from "@electric-sql/pglite/live";import { drizzle } from "drizzle-orm/pglite";
export const pg = await PGlite.create({ dataDir: "idb://my-app", extensions: { live } });export const db = drizzle({ client: pg });With pgwasm:
import { createPgwasm } from "@pgxsinkit/pgwasm";import { drizzle } from "@pgxsinkit/pgwasm/drizzle";import { live } from "@pgxsinkit/pgwasm/live";import { cBuild } from "@pgxsinkit/pgwasm-c";
export const pg = await createPgwasm({ build: cBuild, dataDir: "idb://my-app", extensions: { live } });export const db = drizzle({ client: pg }); // or drizzle(pg)Code that uses the database stays as it was:
const { rows } = await pg.query<{ id: number; title: string }>("SELECT id, title FROM todo WHERE done = $1", [false]);const open = await db.select().from(todo).where(eq(todo.done, false));const watched = await pg.live.query("SELECT count(*) FROM todo", [], (results) => render(results.rows));createPgwasm is always async and resolves once the database is ready; there is no constructor. build
is the compiled Postgres it runs: cBuild is PostgreSQL 18 built with Emscripten, the same kind of build
PGlite ships (see Postgres builds).
What carries over
Section titled “What carries over”| PGlite | pgwasm |
|---|---|
new PGlite(options), PGlite.create(options) |
await createPgwasm({ build: cBuild, ...options }) |
query, sql, exec, transaction |
the same, with the same results (rows, fields, affectedRows, blob) |
query options rowMode, parsers, serializers, blob, onNotice, paramTypes |
the same |
listen, unlisten, onNotification |
the same; to remove a callback, call the function onNotification returns (no offNotification) |
dumpDataDir(), the loadDataDir option |
the same; a backup restores only into an empty data directory (DataDirExistsError) |
waitReady, ready, closed, close(), refreshArrayTypes() |
the same |
extensions: { live } from @electric-sql/pglite/live |
@pgxsinkit/pgwasm/live: live.query, live.incrementalQuery, live.changes; PgwasmWithLive |
drizzle-orm/pglite: drizzle(pg), drizzle({ client: pg }) |
@pgxsinkit/pgwasm/drizzle, both forms |
execProtocol, execProtocolRaw, execProtocolStream, … |
protocol(pg) from @pgxsinkit/pgwasm/protocol |
pgDump from @electric-sql/pglite-tools/pg_dump |
pgDump from @pgxsinkit/pgwasm-pg-dump (pg, args, fileName) |
Repl from @electric-sql/pglite-repl |
Repl from @pgxsinkit/pgwasm-repl (React 19.3 or later) |
dataDir() from @electric-sql/pglite-prepopulatedfs |
prepopulatedDataDir() from @pgxsinkit/pgwasm-c/prepopulated |
amcheck from @electric-sql/pglite/contrib/amcheck |
amcheck from @pgxsinkit/pgwasm-c/contrib/amcheck |
The Drizzle driver needs drizzle-orm 1.0 (the release its peer dependency names), and it never opens a
database of its own: drizzle(), drizzle("…") and drizzle({ connection }) throw
UnsupportedDrizzleConfigError. Create the database with createPgwasm and pass it in.
What you change
Section titled “What you change”dataDir needs a scheme. memory:// (or no dataDir) is in memory, idb://<name> is IndexedDB,
and file://<path> is a directory on disk (Bun). A bare path, which PGlite read as a directory, fails with
UnsupportedDataDirError: write file://./data for ./data. dataDir and fs are exclusive.
Some options moved to the build or to settings.
| PGlite option | pgwasm |
|---|---|
pgliteWasmModule (earlier wasmModule), initdbWasmModule, fsBundle |
createCBuild({ postgresWasmModule, initdbWasmModule, fsBundle }), or all three warmed together in createCBuild({ assets }); pass the result as build |
-c name=value in startParams, lines of postgresqlconf |
settings: { name: "value" } |
initialMemory, initDbStartParams, noInitDb, icuDataDir |
not options |
See warming the artefacts early for createCBuild({ assets }).
Some methods are gone.
clone(): dump and restore instead,await createPgwasm({ build: cBuild, loadDataDir: await pg.dumpDataDir() }).describeQuery(): no equivalent.syncToFs(): every statement persists before it returns, or in the background withrelaxedDurability; the OPFS store’s strict boundary on demand isstrictSync(pg).offNotification(),isInTransaction(),copyToFS(): not on pgwasm.
One open database per store. An idb:// store already open in another context (another tab or
worker) fails with StorageInUseError.
Typed errors. Every runtime error extends PgwasmError, and a statement Postgres rejects throws
DatabaseError with its code and fields. See the error list.
Existing data
Section titled “Existing data”Existing stores are not carried over. Stores created by PGlite or by an earlier pgxsinkit release
are not supported by this release: refresh them. pgxsinkit’s local sync stores are refreshed by destroying
the local store (client.destroy()) and letting it re-sync from the
server. An idb://<name> store needs nothing: pgwasm names its IndexedDB database /pgwasm/<name> where
PGlite and earlier pgxsinkit releases used /pglite/<name>, so the same idb://<name> opens a new, empty
store. Data held only in the old store does not come across: writes that had not synced when the app
upgraded stay behind. Let pending writes flush before upgrading, or take a dumpDataDir() backup.
A backup carries a database across. Take a dumpDataDir() backup with the old version and pass it as
loadDataDir when creating the pgwasm database: the data directory format is unchanged. A PGlite 0.5
backup restores too: PGlite 0.5 runs PostgreSQL 18.3, and the C build runs a later PostgreSQL 18 release,
which reads the same data directory. A data directory PGlite wrote carries no build record, and the C build
claims it (see A store belongs to its build).
const backup = await oldPg.dumpDataDir(); // on PGlite 0.5, or an earlier pgxsinkit releaseconst pg = await createPgwasm({ build: cBuild, dataDir: "idb://my-app", loadDataDir: backup });The old databases stay until you delete them. pgwasm never deletes /pglite/* IndexedDB databases:
PGlite uses the same names, and another app on the same origin may own them. Once an app’s own old stores
are no longer needed, it can delete them by name, or list them with indexedDB.databases() where the
browser supports it:
indexedDB.deleteDatabase("/pglite/my-app"); // the store that was idb://my-appEarlier PGlite versions need a dump and restore. PGlite 0.2 runs PostgreSQL 16 and PGlite 0.3 and 0.4
run PostgreSQL 17; PostgreSQL 18 does not open their data directories. Dump with PGlite’s pg_dump and run
the SQL into a new pgwasm database:
import { pgDump } from "@electric-sql/pglite-tools/pg_dump";
const dump = await pgDump({ pg: oldPg }); // on PGliteconst pg = await createPgwasm({ build: cBuild, dataDir: "idb://my-app-v2" });await pg.exec(await dump.text());A plain dump leaves the restoring session’s search_path empty; set it back if the session relies on it.
opfs-ahp:// stores need a backup. pgwasm does not read PGlite’s one-handle-per-file OPFS layout:
opfs-ahp:// fails with OpfsAhpRemovedError. Take a dumpDataDir() backup on PGlite 0.5 (a SQL dump on
earlier versions) and pass it as loadDataDir when creating the pgwasm database. pgwasm’s OPFS store is
createOpfsPgwasm from @pgxsinkit/pgwasm/opfs, which runs in a worker and keeps a data directory in four
files (see the OPFS-repacked store):
import { createOpfsPgwasm } from "@pgxsinkit/pgwasm/opfs";
const backup = await oldPg.dumpDataDir(); // on PGlite 0.5const pg = await createOpfsPgwasm({ build: cBuild, directory, pgwasm: { loadDataDir: backup } });Where pgwasm does not fit
Section titled “Where pgwasm does not fit”- Runtimes. pgwasm is ESM only, for Bun and browsers (pages, dedicated workers, SharedWorkers and extension pages). Node, Deno and CommonJS are not supported.
- Browsers. The C build runs in Safari and iOS 18.4, Chrome 137, Firefox 131 and later releases (see Supported browsers).
- Extensions.
amcheckis the only extension. PGlite’s other contrib modules (such ascitext,bloomorcube) and its separately published extensions (such as pgvector or PostGIS) are not available. - Workers. There is no drop-in equivalent of
PGliteWorker. pgxsinkit’s worker mode runs the local store of its own sync client, not a general-purpose pgwasm database. - Framework bindings. pgwasm has no React or Vue bindings of its own like
@electric-sql/pglite-reactor@electric-sql/pglite-vue. pgxsinkit’s React hooks serve its sync client.