In July 2024, the team behind ElectricSQL did something most companies never recover from: they deleted their own product. The old Electric was a vertically integrated local-first stack with embedded SQLite, a bidirectional replication protocol, conflict resolution, permissions as database rules, and client generation. The rewrite, shipped under the name Electric Next, threw most of it away. Gone was the part local-first had always been famous for: writing freely on the client and letting the system merge those writes back into the database for you.
That looked like a retreat at the time. With two years of distance it reads as the moment the category found its architecture. Zero, from the Replicache team, never shipped the merge-everything model. TanStack DB never tried. By 2026 all three serious sync engines have converged on the same shape: sync reads out of Postgres, run them locally, and hand writes back to a server that stays authoritative. The browser stopped being the source of truth.
What local-first actually promised
The original pitch was seductive. Put a real database in the browser, let the application read and write against it at memory speed, and run a sync process in the background that reconciles local state with a server and with every other client. Reads are instant because they never leave the device. Writes feel instant because they apply locally first. Offline is free because the local copy is the application's primary store. The network becomes an implementation detail.
Martin Kleppmann's 2019 essay gave the idea its name and its seven ideals, and conflict-free replicated data types (CRDTs) gave it a theoretical engine: data structures that can be edited concurrently on many devices and merged deterministically, with no central coordinator and no conflicts. On paper, CRDTs make the hard part disappear.
In practice the hard part moved rather than vanished. A CRDT guarantees that concurrent edits converge to some value. It does not guarantee that value respects your invariants. Two users each decrementing an inventory count from 1 will both succeed locally, and the merged result is a negative balance no business rule allows. Permissions are worse. If every client can write anything and merges happen automatically, authorization has to be re-expressed as rules the merge engine can enforce, which is exactly the kind of clever database-rule system Electric built and later judged too costly to keep.
The read path is the part that worked
Strip away the merge fantasy and one piece of local-first holds up cleanly: streaming a subset of the database to the client and keeping it live. Every current engine builds on the same foundation here, Postgres logical replication. The database emits a change feed, the sync engine consumes it, and clients subscribe to slices of that feed.
Electric calls its slice a Shape: a partial replica of one table defined by a where clause. A client subscribes over plain HTTP, gets an initial snapshot, then receives an append-only log of changes. Because it is HTTP, the responses cache in a CDN and the server stays stateless. Electric's 1.1 storage engine, shipped in August 2025, exists precisely because that read fan-out became the scaling bottleneck once writes were out of scope.
import { ShapeStream } from "@electric-sql/client"
// A shape is a live partial replica: one table, filtered by a where clause.
const issues = new ShapeStream({
url: "https://api.example.com/v1/shape",
params: {
table: "issues",
where: "project_id = '8f3a' AND archived = false",
},
})
issues.subscribe((messages) => {
// initial snapshot, then an append-only stream of inserts/updates/deletes
})Zero takes a different route to the same destination. Its server component, zero-cache, replicates Postgres into a SQLite replica and clients query through ZQL, a TypeScript query language that reads like Drizzle or Kysely. The same query runs against the local cache for an instant answer and against the server to keep the cache fresh. The connection is a WebSocket, not cacheable HTTP, which buys lower latency at the cost of stateful infrastructure.
TanStack DB sits one layer up. It does not mandate a transport at all. Its collections hold normalized data, and its live queries run through d2ts, a TypeScript implementation of differential dataflow. A query over 100,000 in-memory rows recomputes only the rows that changed when the underlying data updates, which keeps re-renders close to a millisecond.
The 0.5 release in November 2025 added the piece that made it practical at scale: query-driven sync. Instead of loading a whole collection up front, the engine reads the predicates of your live query, the where clause, the order, the limit, and turns the query itself into the API request. By 0.6 in March 2026 it had layered SQLite-backed persistence and offline support on top, while still sitting in beta on the road to 1.0.

The write path is where the dream died
Reads fan out cleanly because a read changes nothing. Writes are the opposite. A write has to be validated, authorized, ordered against other writes, and made durable, and it has to do all of that while the user believes it already happened. This is the problem CRDTs promised to dissolve and the problem every engine now solves the same boring way.
Electric was blunt about why it backed out. From the Electric Next announcement:
Coming from a research background, we prioritised optimal solutions over practical ones. There was a danger of building a system that demos well in a constrained setting but that never actually scales out reliably.
So Electric drew a hard line. It does read-path sync and nothing else. The documentation says it plainly: Electric does not do write-path sync, and it does not prescribe a solution for getting data back into Postgres from local apps. You write to your own REST endpoints, the same POST and PUT and DELETE routes you already run, with the same authorization middleware. The change lands in Postgres, replicates back through the shape, and the optimistic value is replaced by the canonical one.
Zero reaches the same place with more machinery. Its custom mutators are TypeScript functions with two halves. The client half runs immediately against the local SQLite replica so the UI updates with no round trip. The server half runs against Postgres and is authoritative. When the server result arrives, the client discards its speculative version and rebases its local history on top of the canonical one.
// One mutator, two execution contexts.
// Client: optimistic, against local SQLite. Server: authoritative, against Postgres.
export function createMutators(authData: AuthData) {
return {
issue: {
async close(tx, { id }: { id: string }) {
// runs on the client for instant feedback,
// then again on the server, where this check actually counts
if (!authData.canEdit(id)) throw new Error("forbidden")
await tx.mutate.issue.update({ id, status: "closed" })
},
},
}
}The shared-function trick is the clever part, but notice what it concedes: the server always wins. The client computation is, in Zero's own framing, speculative and discarded the moment the server speaks. That is not bidirectional sync. It is optimistic UI with a durable rebase, the same pattern that server actions and typed RPC have been circling for years.
TanStack DB makes the concession explicit in its API surface. A collection is configured with onInsert, onUpdate, and onDelete handlers. The mutation applies optimistically to the local collection, the handler calls your backend, and the engine reconciles when the server confirms.
import { createCollection } from "@tanstack/db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
const todos = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: () => api.todos.list(),
getKey: (todo) => todo.id,
onInsert: async ({ transaction }) => {
const { modified } = transaction.mutations[0]
await api.todos.create(modified) // your API stays the source of truth
},
}),
)Three engines, three vocabularies, one architecture. The local copy is a cache you can read and edit at speed. The server is still the database of record. Conflicts get resolved by the simplest rule there is: whoever owns the write decides.

Where the three engines actually differ
Converging on server-authoritative writes does not make these tools interchangeable. They disagree on transport, on how much backend they require you to run, and on how far they push toward genuine offline. Those differences map directly onto operational cost.
| Concern | ElectricSQL | Zero | TanStack DB |
|---|---|---|---|
| Read primitive | Shapes: partial replica, one table plus a where clause | ZQL queries against a synced SQLite replica | Query-driven sync into in-memory collections |
| Transport | HTTP, CDN-cacheable, stateless | WebSocket, stateful | Transport-agnostic (fetch, Query, or a sync collection) |
| Write path | Your own API. Electric writes nothing. | Custom mutators, server half authoritative | Optimistic handlers that call your API |
| Extra infra to run | Electric sync service beside Postgres | zero-cache replica plus a push endpoint | None required; backend-agnostic |
| Offline writes | Do it yourself (through-the-database pattern) | Queued locally, rebased on reconnect | Via 0.6 persistence; depends on the collection |
| Where validation lives | Your API layer | Server mutator | Your API or handlers |
Read the bottom three rows together and the spectrum is obvious. Electric asks the least of your architecture and gives you the least: a fast read stream you bolt onto an app you already wrote. Zero asks for a dedicated replica process and a push endpoint, and in return it owns offline write queuing and rebasing for you. TanStack DB runs no server of its own and leaves the sync source as your choice, including Electric or Zero underneath it.
What this means for the app you are actually building
The first implication is the most freeing. You do not have to adopt a new database or rewrite your API to get most of the benefit. The read path replaces the tangle of fetch calls, cache invalidation, and polling that data fetching has become. It does not replace your ORM, your migrations, or your write endpoints. Those stay exactly where they are.
The second implication is the one teams underestimate: partial replication is now your design problem, and it is a real one. A shape's where clause and a collection's query predicate are not just performance knobs. They are security boundaries. A client that subscribes to a shape receives every row that matches, so the filter has to encode what that user is allowed to see. Get it wrong and you have shipped a data leak that no amount of UI permission checking will catch.
A few constraints worth internalizing before you commit:
- Offline is a spectrum, not a checkbox. Instant reads from a warm cache are easy. Durable offline writes that survive a refresh and rebase cleanly on reconnect are hard, and only Zero treats them as a first-class default.
- You are running more infrastructure. Electric's sync service and Zero's zero-cache are stateful processes sitting next to your database, reading its replication stream. That is another thing to deploy, monitor, and scale, with its own failure modes.
- Multi-row invariants stay on the server. Anything that needs a transaction across rows, a balance that cannot go negative, a unique slug, a seat count, belongs in the authoritative write, never in the optimistic client copy.
The third implication is about latency honesty. Optimistic UI hides the round trip, and that is genuinely good right up until the server rejects the write. Zero's own guidance is that optimistic mutations only make sense when success is the common case, because a rollback that yanks back what the user just saw is more jarring than a brief spinner. Validating at the write boundary, the same place schema validation already lives, keeps the rejection rate low enough for the illusion to hold.
The trade the category finally made
Local-first set out to abolish the server. What survived is narrower and far more useful: a synced read replica in the browser, optimistic writes for feel, and a server that never stopped being in charge. The CRDT engines did not lose because the math was wrong. They lost because invariants and authorization live above the merge layer, and no amount of automatic convergence enforces a rule the data structure cannot see.
What is genuinely unsolved is the long-offline write. Every current engine assumes reconnection happens soon enough that rebasing is cheap and conflicts are rare. Push the offline window out to days, or let two users edit the same record while both are disconnected, and you are back to the merge problem the category just walked away from. For collaborative documents that gap is being filled by purpose-built CRDT libraries like Yjs, working alongside these engines rather than replacing them.
So the open question is not whether sync engines work. They do, for the read-heavy, mostly-online applications that are most of what we build. The question is whether anyone cracks durable, conflict-tolerant offline writes without dragging back the complexity that sank the first generation. Until someone does, the server owns your writes, and after a decade of trying to change that, the tooling has decided that is fine.
