PGlite, Electric SQL, and Triplit do not solve the same layer. This guide shows where data lives, how writes travel, who resolves rejected or concurrent changes, and which operational seam your team must own.
The short answer: choose the layer first
- Choose PGlite when you need a real PostgreSQL-compatible runtime inside a browser, Node.js, or Bun process and are prepared to bring your own multi-device write sync.
- Choose Electric when Postgres is authoritative and you need a read path that streams selected server data into local application state. Your application still owns the write path.
- Choose Triplit when you want an integrated client/server database with optimistic local writes, an offline outbox, sync, and reactive queries—and you accept its storage, migration, licensing, and server-operating model.
The key distinction is responsibility. PGlite gives you local SQL. Electric gives you Postgres-to-client Shapes. Triplit gives you a coordinated local cache, write queue, and sync server. None removes the need to define authorization, uniqueness, validation, and what the user should see after a rejected write.
Decision matrix
| Decision | PGlite | Electric SQL | Triplit |
|---|---|---|---|
| Local durable store | PGlite database persisted with IndexedDB or a supported filesystem | Determined by the client adapter or local state layer you pair with Shapes | IndexedDB in browsers when explicitly configured; memory storage is not reload-durable |
| Server authority | Optional; PGlite can be entirely local | Existing Postgres database | Triplit server and its synced database |
| Write path | Local SQL; remote write sync is yours to design | Separate application-selected path back to Postgres | Client mutations update the local cache and queue for server sync |
| Core sync direction | No built-in multi-device sync | Postgres read path to clients | Client/server synchronization in both directions |
| Conflict and rejection owner | Your application or a separate sync system | Your API, optimistic-state layer, and domain merge policy | Triplit converges synced properties; your product still owns rejected writes and domain invariants |
| Browser constraints | Storage quotas, filesystem support, worker lifecycle, and single-connection coordination | Depends on the local state/persistence pattern; the service itself also needs durable operator state | Memory loses cache/outbox on reload; IndexedDB naming and lifecycle need deliberate setup |
| License boundary | @electric-sql/pglite npm metadata is Apache-2.0; PGlite docs also carry a PostgreSQL license notice | @electric-sql/client and the Electric repository are Apache-2.0 | @triplit/client npm metadata is AGPL-3.0-only, and the repository root is AGPL-3.0 |
| Operator burden | Low for a local-only tool; higher once you add remote sync | Operate Postgres plus Electric service state and your write API | Operate or buy the coordinated Triplit server path and plan version migrations |
This table is intentionally architectural rather than numeric. Download counts, stars, and synthetic bundle or startup figures do not tell you which responsibility model fits your product.
PGlite: local PostgreSQL, not a sync protocol
PGlite packages PostgreSQL as WebAssembly for browser, Node.js, and Bun environments. It is useful when SQL compatibility is the local application primitive: complex queries, transactions, indexes, and an ecosystem that already understands Postgres.
A durable browser database can use IndexedDB:
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite("idb://local-workspace");
await db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
body TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
That example proves local persistence, not multi-device synchronization. A second device has a different local database until your application supplies a replication or write-sync design.
Browser and worker constraints
PGlite's filesystem documentation distinguishes memory, IndexedDB, and filesystem-backed choices. Browser support is not uniform: the documented OPFS access-handle path is not supported by Safari, so a cross-browser product needs a tested fallback rather than a blanket OPFS assumption.
PGlite also uses a single database connection. Its multi-tab guidance coordinates tabs through a shared worker where supported or a leader-election worker pattern elsewhere. Treat worker startup, tab handoff, storage eviction, and schema upgrades as application lifecycle concerns.
What the Electric plugin currently does
The documented PGlite Electric sync plugin is still labeled alpha. It syncs an Electric Shape into a PGlite table, but the documentation explicitly says it does not yet sync local writes outward or provide conflict resolution. Use it as a read-sync building block, not as evidence that PGlite has become a complete bidirectional local-first database.
PGlite fits best when local SQL is the non-negotiable requirement and the remote sync seam is either unnecessary or intentionally owned elsewhere.
Electric SQL: Postgres read-path sync with an application-owned write path
Current Electric sync streams subsets of Postgres data called Shapes over HTTP. Postgres remains authoritative. This is different from the older mental model that Electric automatically accepts offline client writes and resolves them through a universal CRDT write layer.
The client subscribes to data; your system decides how changes get back to Postgres:
import { Shape, ShapeStream } from "@electric-sql/client";
const stream = new ShapeStream({
url: `${ELECTRIC_URL}/v1/shape`,
params: { table: "projects" },
});
const projects = new Shape(stream);
projects.subscribe(({ rows }) => {
renderProjects(rows);
});
The four documented write patterns
Electric's write-path guide describes four broad choices:
- Online writes. Send a mutation through your API to Postgres, then let Electric stream the committed result back. This is straightforward, but the user cannot complete the write while offline.
- Ephemeral optimistic state. Show a local optimistic result while the API request is pending. This improves perceived latency, but component-scoped state can disappear on navigation or reload.
- Persistent optimistic state. Keep optimistic writes in a durable shared store. This supports offline work, but you must merge optimistic and synced state, retry delivery, rebase later writes, and explain rejection.
- Through-the-database write sync. Record local changes in a database-backed change log and synchronize them to the server. This can provide a deeper local-first model, but it adds another protocol and operational seam rather than making writes an automatic property of Electric Shapes.
For the offline patterns, delayed rejection is a product behavior, not just an error log. If the server rejects an authorization change, uniqueness violation, or stale update after dependent local edits have accumulated, your application needs a rollback, rebase, quarantine, or manual-resolution policy.
Self-hosting responsibilities
Electric's deployment documentation requires Postgres logical replication and durable service state. Backups, replication slots, service-disk persistence, restore order, and shape-cache behavior belong in the operating plan. "Uses existing Postgres" does not mean there is no additional stateful service to manage.
Electric fits best when server-owned Postgres data and flexible application write APIs are strengths—not when the goal is an all-in-one offline mutation engine.
Triplit 1.0: integrated local cache, outbox, and sync
Triplit combines a typed client database, local cache, optimistic mutations, a server, and a CRDT-based sync protocol. Compared with PGlite and Electric, it owns more of the end-to-end path.
Triplit's official repository documentation says offline mutations update the local cache immediately and enter an outbox for later delivery. Persistence still needs an explicit choice:
import { TriplitClient } from "@triplit/client";
const client = new TriplitClient({
schema,
serverUrl: process.env.NEXT_PUBLIC_TRIPLIT_SERVER_URL,
token: sessionToken,
storage: "indexeddb",
});
The default memory storage can support optimistic reads and writes during the current page lifetime, but its cache and pending outbox do not survive a reload. Configure IndexedDB for browser durability, use deliberate database names when multiple clients/projects can coexist, and test upgrade and eviction behavior.
Pre-1.0 systems need a coordinated migration
Triplit 1.0 changed query APIs, local storage format, and the sync protocol. Its official 1.0 migration source says clients and servers must be updated together because 1.0 is not backward-compatible with pre-1.0 counterparts; self-hosted server upgrades also involve data migration.
That makes a rolling compatibility plan important. Inventory old client versions, decide whether offline clients can reconnect after the cutover, migrate server data, and verify that old local stores cannot silently rejoin with an incompatible protocol. Examples written for pre-1.0 APIs should not be copied into a current implementation.
License boundary
The current npm metadata for @triplit/client declares AGPL-3.0-only, and the Triplit repository root carries the AGPL-3.0 license. The published @triplit/server and @triplit/db metadata checked for this refresh did not expose the same explicit license field, so do not turn that absence into a permissive-license claim. Review the exact packages, deployment model, and obligations with counsel for your distribution and network-use scenario.
Triplit fits best when its integrated data model is an advantage and your team is ready to operate the coordinated server, storage, migration, and license seams.
Conflict semantics: convergence is not business correctness
"Conflict-free" can describe deterministic data convergence while leaving important product conflicts unresolved. Evaluate at least four separate layers:
- Authorization: Was the user allowed to make the write when the server evaluated it?
- Uniqueness and invariants: Can two offline users reserve the same scarce resource or create an invalid cross-record state?
- User intent: If two values converge deterministically, does the result preserve what either user meant?
- Rejection recovery: What happens to later local actions that depended on a write the server rejects?
PGlite leaves all remote merge semantics to your sync design. Electric leaves write merge, validation, rejection, and rollback to the selected application pattern. Triplit provides property-level CRDT convergence for its sync path, but the product still owns domain invariants, permissions, and user-facing recovery.
If the primary requirement is concurrent document editing rather than database records, compare dedicated Yjs, Automerge, and Loro CRDT libraries instead of treating a database sync layer as a text-editing CRDT.
Architecture verdicts
Choose PGlite for a local SQL application
Pick PGlite for browser analysis tools, local development utilities, and offline-capable products where PostgreSQL semantics are valuable and remote sync is either absent or separately designed. Budget for workers, storage lifecycle, and schema migrations.
Choose Electric for Postgres-owned data and flexible writes
Pick Electric when an existing Postgres database is authoritative, clients benefit from live partial replication, and your team wants to choose its own write API and optimistic-state strategy. Define rejection and rollback before promising offline writes.
Choose Triplit for an integrated TypeScript sync stack
Pick Triplit when local optimistic mutations, a durable outbox, reactive queries, and coordinated client/server sync should come from one system. Use IndexedDB for reload durability, plan pre-1.0 migrations as a tandem cutover, and review the exact AGPL boundary.
Combine layers only with an explicit responsibility map
A PGlite-plus-Electric design can be sensible when local SQL and Postgres Shapes are both required, but the current plugin remains alpha and read-only. Adding a second write-sync mechanism can also be sensible, provided one component clearly owns mutation identity, retries, ordering, merge, authorization, and rejected-write recovery.
For adjacent database options, compare TinyBase, WatermelonDB, and RxDB for offline-first storage or TanStack DB, Zero, and LiveStore as sync engines.
Methodology and update risk
This refresh used official PGlite and Electric documentation, npm registry metadata, official GitHub releases, and Triplit's repository, migration source, package metadata, and license. Sources and volatile package state were rechecked on July 24, 2026.
Update-risk note: At that check,
@electric-sql/pglitewas 0.5.4,@electric-sql/clientwas 1.5.24, Electric's sync-service release was 1.7.8, and@triplit/clientwas 1.0.50. The PGlite Electric plugin still described itself as alpha and said outbound local writes and conflict resolution were not supported.
These versions are evidence timestamps, not ranking inputs. Recheck package versions, plugin status, migration notes, and license metadata before implementation. Triplit's former public documentation URLs returned HTTP 410 during research, so this guide links to the live official repository sources rather than dead documentation routes. No bundle-size, startup-time, popularity, pricing, privacy, security, or performance winner is claimed without a reproducible current test.
