pgxsinkit is an offline-first sync toolkit for a PostgreSQL -> ElectricSQL -> PGlite read path and a client -> write API -> PostgreSQL write path. The @pgxsinkit/* packages are the product; the demo app (apps/board), the minimal reference server (apps/write-api), and the integration + performance harness exist to prove and harden them.
Canonical timestamps are stored as bigint microseconds since the unix epoch and cross API/sync boundaries as decimal strings.
📖 Documentation — start with What is pgxsinkit?, then Getting started and Core concepts. Before you ship, read Operating in production — the runtime gotchas (convergence cadence, edge cold starts, the browser HTTP/2 connection budget) that decide whether a live app feels fast.
pgxsinkit row filters may use cross-table subquery where clauses — for example membership
fan-out, where a row in a container streams to every member of that container:
container_id IN (SELECT container_id FROM memberships WHERE member_id = <subject>)The electric-proxy forwards this verbatim as the Electric shape where, so streaming it relies on
a required ElectricSQL capability:
- ElectricSQL >= 1.7 running with
ELECTRIC_FEATURE_FLAGS=allow_subqueries,tagged_subqueries.
This is a hard prerequisite, not an optional optimisation. Subquery where support is a flagged
preview feature (still flagged as of 1.7.3); without the flag Electric rejects any subquery where
with HTTP 400 ({"where":["Subqueries are not supported"]}). The sync then fails closed — no rows
stream — it never silently fans out unfiltered data.
On managed Electric Cloud the subquery preview is currently activated per source by Electric staff on request (no self-serve toggle yet; ElectricSQL intends to make it the default) — so ask Electric to enable it for your source, or self-host Electric with the flag set. A self-hosted stack sets the flag directly.
A second point follows from the same grammar: a PostgreSQL enum column referenced in a shape
where must be cast to text — "role"::text = 'manager', not "role" = 'manager'. The enum
column itself stays an enum everywhere else — RLS and the write path keep using it natively, so there
is no enum→text migration. See
The Electric subquery requirement for the
full story.
bun add @pgxsinkit/client @pgxsinkit/server @pgxsinkit/contracts
# React bindings (optional): bun add @pgxsinkit/react
# Constant-handle OPFS storage (optional): bun add @pgxsinkit/pglite-opfs-repackedThe packages are published to public npm; install them with whichever package manager you use
(pnpm add, npm install, yarn add — pgxsinkit mandates none). Then follow
Getting started to wire the read and write
paths and provision the in-database apply function.
The substantial example (apps/board, a Linear-style board + chat) drives the full read and write
paths end-to-end against a partial Supabase + Electric stack:
mise installbun installcp .env.example .envmkcert -install— one-time: trust the local CA so the browser accepts the gateway's HTTP/2 certbun run infra:up— brings up the board stack (partial Supabase + Electric), builds the edge functions, and applies the board's migration historybun run seed:board— GoTrue identities + fixturesbun run dev:board
The board stack is self-contained on its own ports (gateway 54331, db 54322, electric 54330,
HTTP/2 gateway 54343), so it coexists with the harness. Studio is at http://localhost:54333. For
the minimal reference server (apps/write-api) instead, use bun run infra:harness:up (PostgreSQL +
Electric) → bun run dev:api.
There is exactly one write path: client writes are staged locally, flushed through the write API,
and applied to PostgreSQL in a single in-database PL/pgSQL function (pgxsinkit_apply_mutations).
There is no selectable backend — the in-database bulk apply is the only strategy. See
The write path and
ADR-0002.
Contributor setup, the canonical vocabulary, and the agent guide live in AGENTS.md
and CONTEXT.md. The repository is a Bun workspace:
apps/board— the substantial demo (Linear-style board + chat) on a partial Supabase + Electric stack.apps/write-api— the minimal@pgxsinkit/serverreference (Bun, no web framework).packages/contracts·client·server·react— the published sync toolkit.packages/pglite-opfs-repacked— the published OPFS storage engine for PGlite.packages/schema,packages/board-schema— example/demo registries (your app defines its own).infra/,tests/,supabase/functions— compose stacks, suites, and the demo's edge functions.
Scripts are check-default (a bare verb never mutates):
bun run validate # fast pre-commit gate: format, lint, typecheck, fast unit subset
bun run validate:full # pre-push + CI gate: adds the PGlite-backed unit suite
bun run test:integration # container-backed suites on isolated, ephemeral compose stacksFresh clone: @electric-sql/pglite is temporarily overridden to the @pgxsinkit/pglite fork (see
docs/runbooks/pglite-fork-override.md). The fork is mirrored
on public npm, so a plain bun install resolves it — no registry auth or extra setup needed.
Deeper references, all under docs/: architecture ·
testing strategy · migrations ·
function artifacts · performance.
@pgxsinkit/* publishes from a semver tag: CI derives the version from the tag and publishes all
packages at that one version — there is no version bump. See RELEASING.md and
ADR-0001.
MIT © pgxsinkit contributors.