Multi-tab behavior
Every tab that opens the same PGlite database shares one set of files, and therefore one
supapower.changes queue. Exactly one tab may drain it, otherwise the same batch is pushed twice.
Setting it up
Section titled “Setting it up”Opening a plain PGlite instance against the same dataDir from several tabs risks corrupting the
database no matter what Supapower does with the queue - only ever touch the files from one process,
using a worker.
@supapower/worker is the
more stable option: one SharedWorker hosts the database for every tab instead of one tab owning it,
falling back automatically to PGlite’s own dedicated worker where SharedWorker isn’t available.
import { worker } from '@supapower/worker/worker';import { IdbFs, PGlite } from '@electric-sql/pglite';
worker({ async init() { const pg = new PGlite({ fs: new IdbFs('your-app'), relaxedDurability: true, });
// Preferably run any database migrations here...
return pg; },});import { createPGliteWorker } from '@supapower/worker';import { supapower } from 'supapower';
export const pg = await createPGliteWorker( { shared: () => new SharedWorker(new URL('./pglite-worker.ts', import.meta.url), { type: 'module' }), fallback: () => new Worker(new URL('./pglite-worker.ts', import.meta.url), { type: 'module' }), }, { id: 'your-app', extensions: { supapower } },);You can use PGlite’s own dedicated multi-tab worker
instead if you’d rather not add the extra package - the setup is the same shape, just without the
SharedWorker fallback, and id is optional rather than required. The downside with this approach is that queries may be frozen in non-leading tabs if the browser decides to save resources by throttling or suspending background tabs (i.e. the leader tab in that case).
With the SharedWorker approach above this problem is mitigated, as the shared worker continues to run independently of individual tabs, ensuring that queries are not frozen even when the user switches away from the tab that initially started the worker.
How leadership works
Section titled “How leadership works”That lock cannot live in Postgres. PGlite is a single-connection engine and every tab runs its own
instance, so pg_advisory_lock() is invisible to the other tabs and would block the only connection
this one has. Supapower coordinates in the browser instead, and picks the strongest mechanism the
runtime offers:
leadership |
When | Mechanism |
|---|---|---|
visible-tab |
A browser tab | An exclusive Web Lock named supapower:outgoing:<scope>, held only while the tab is visible |
web-lock |
A browser context without a document (e.g. a worker) |
The same lock, held unconditionally |
single-process |
Node, Bun, Deno | None - a second process is assumed not to exist |
Leadership moves on its own when the leading tab is hidden, closed or crashes: a Web Lock is
released by the browser when the tab closes or crashes, and a visible tab gives it up the instant
it is hidden. A hidden tab never drains the queue - browsers throttle or freeze timers in hidden
tabs, so a hidden leader would stall the queue for every tab - and no tab drains while every tab is
hidden. Read sync.leadership to see which mechanism you ended up with.
Leadership moving between tabs starts a fresh syncer each time, but it does not mean a fresh
download every time: downloadThrottle skips a table whose last download is still within the
window, so switching tabs repeatedly inside that window re-downloads nothing.