Connection · Interrupted

Something didn't load

Part of this page failed to reach you. Reload to try again — if it keeps happening, check your connection.

Skip to main content
Engineering14 min read

The Server Is a Sync Relay Now: Architecting Around Client-Owned State

At QCon London 2026 Kleppmann described local-first as the best of Google Sheets and the best of Git, and this year the sync engines shipped. From my reading of Electric, Zero, and LiveStore side by side, these are my notes on what changes when the client's copy becomes primary: where conflicts resolve, what the server still owns, and the merge-vs-refuse line that decides which apps must never be built this way.

All Posts
Listen to this post
0:00 / 0:00
2/4

Every replicated system has one honest answer to a simple question: when two copies of a record disagree, which copy wins? For most of the web's history the answer has been the server. The database behind the API holds the primary copy; everything on a phone or a laptop is a cache that can be discarded and refetched. An edit that has not reached the server has, for all practical purposes, not happened.

Local-first software inverts that answer, and the inversion is the whole architecture. At QCon London 2026, Martin Kleppmann pitched local-first as "the best of Google Sheets and the best of Git": real-time collaboration on rich data, combined with a full primary copy of your work on your own machine. The cloud does not disappear in this picture — it gets demoted. Sync and backup remain; authority leaves.

What pushed me to map this properly is that 2026 is the year the idea stopped being a research agenda. ElectricSQL streams Postgres data into clients at CDN scale. Rocicorp's Zero ships speculative client writes with server rebase. LiveStore puts an event-sourced database inside the browser and makes the server a relay for the log. Three shipping engines, all marketed with the same "local-first" label — and, as I found when I read their sync documentation side by side, three different answers to where truth lives. This post is my attempt to write down what actually changes in a system's architecture when the client owns the state, what the server still owns, and the class of applications that must not be built this way.

Where the truth actually moves

The 2019 Ink & Switch essay that coined the term is explicit about the mechanism. In a cloud app, the server holds the primary, authoritative copy and every client render is subordinate to it. In a local-first app, the copy on the user's device becomes primary. "Servers still exist, but they hold secondary copies of your data." The devices are the replicas of record; the data center holds a replaceable mirror.

Git is the reference point that makes this concrete. A local clone is not a cache of GitHub — it is a complete primary copy, subordinate to no server. GitHub is a rendezvous point: convenient for exchange, backup, and discovery, and replaceable by any other host without loss. Local-first asks for that property in interactive applications, plus the one thing Git never gave: fine-grained real-time merge, which is where CRDTs and libraries like Automerge come in. I have written before about why CRDT replicas converge — the merge function, not the network, carries the guarantee — so I will not repeat the mechanics. This post is about the system around the merge.

The inversion is easier to see as a data flow than as a definition. In the diagram below, the left side is the architecture I have built for most of my career; the right side is what a local-first deployment of the same product looks like.

Four consequences fall out of moving the primary copy, and each one rewrites part of the backend's job description.

First, reads stop touching the network. Every query runs against a local store, so interaction latency is bounded by disk and memory, not by a round trip. The p99 of a read is no longer a backend SLO — it is a property of the device.

Second, write acceptance stops being the server's decision. A write commits locally and durably before any remote party hears about it. There is no moment at which a server can reject the write and pretend it never happened; by the time the relay sees it, the user has moved on.

Third, and following directly from the second: conflict handling moves from write time to sync time. A server-authoritative system resolves contention by ordering writes through one database and rejecting losers immediately. A local-first system accepts everything at the edge and reconciles later — which means the data model must be designed so that reconciliation has an acceptable answer for every possible interleaving.

Fourth, the provider becomes swappable. Because the relay holds secondary copies, switching hosts is a re-point, not a migration. This was the political core of Kleppmann's keynote: with AWS, Azure, and Google Cloud holding roughly seventy percent of the European market, and incidents like the ICC prosecutor losing access to his Microsoft-hosted email under US sanctions, he framed the demoted server as risk mitigation — the impact of a sudden provider lockout drops from existential to inconvenient.

Three engines, three answers to who owns the truth

The part that surprised me most in my reading is how differently the current sync engines place authority, given that all three get described with the same label.

ElectricSQL does not move authority at all. Electric is, by its own docs, a read-path sync engine for Postgres: it consumes the logical replication stream, fans rows out into "shapes" — partial replicas of a table filtered by a WHERE clause — and delivers them to clients over plain HTTP so CDNs can handle fan-out. Writes are explicitly not its job. They travel through your existing API into Postgres, which remains the source of truth. What Electric buys is local-first read behavior — instant queries, live updates, offline reads — layered over a classically authoritative backend. If the product question is "can my dashboard feel instant," this is the least invasive answer.

Zero, from Rocicorp, keeps authority on the server but lets the client borrow it for a moment. Every write is a named mutator implemented twice: a client version that runs instantly against the local store, and a server version that is authoritative. The client's result is speculative — when the server's result arrives, pending local effects are rolled back and reapplied on top, a rebase in all but name, and the speculative result is discarded. The two implementations are allowed to differ, which is the point: the server copy can enforce permissions and validation that no client can be trusted to run. Zero's docs call the technique server reconciliation and credit multiplayer games, which have shipped it for decades. Architecturally this is optimistic UI formalized into the sync protocol: the client renders a prediction; the server still decides.

LiveStore is the full inversion. State lives in an event-sourced store on the client: an append-only eventlog is the source of truth, and the SQLite tables an application queries are materialized views, rebuilt from events by materializer functions. Sync is push/pull in the Git sense — a client must pull the latest upstream events and rebase its unpushed ones before it may push. The server's role collapses to ordering and relaying the log. The consequence I find most under-appreciated: the relational schema stops being the durable contract, because tables can be rebuilt at will. The event schema is the contract now, and it inherits every forever-compatibility problem that event-sourced backends already know.

Lined up this way, "adopting local-first" turns out to be an underspecified requirement. Electric keeps server truth and syncs reads. Zero keeps server truth and simulates client truth for the length of one server round trip. LiveStore and the Automerge lineage make client truth real. These are not three vendors competing on the same design — they are three different placements of the authoritative copy, and the placement, not the vendor, is the decision. It is also not one decision per application: nothing stops a system from syncing its documents LiveStore-style while its billing table stays behind a request/response API. Deciding per collection is, in my judgment, the only version of this that survives contact with a real product.

What the server still owns after the demotion

Demotion is not deletion. The Ink & Switch authors were careful to say the difference is a change in the server's responsibilities, not its absence, and their list from 2019 still reads like a deployment checklist for the 2026 engines.

The first job is store-and-forward relay. Two collaborators are rarely online at the same instant; peer-to-peer sync dies on the closed laptop. A small always-on service that accepts encrypted changes and holds them until the other side reconnects is what makes asynchronous collaboration work at all — this is exactly the sync backend Electric, Zero, and LiveStore each require.

The second is durable backup. Phones are storage-limited and laptops die; an archival copy of the log or the documents belongs in a data center, with the crucial property that it is a secondary copy — restorable from any client, replaceable by any competing host.

The third is the boundary work: identity, discovery, and authorization at the sync layer. The relay authenticates who may subscribe to which partition of data, which is also where the practical permission model lives. Electric scopes access by shape definition; a Zero server mutator can reject a write outright. What no engine can do is enforce revocation retroactively — bytes already synced to a peer's disk are that peer's bytes. The 2019 essay flagged fine-grained access control in a converged data structure as an open research question, and from what I can tell reading the current engines' docs, it still is: the deployable pattern today is coarse — partition your data so that sync scope equals permission scope, and treat anything finer as unsolved.

The fourth is being the bridge: calling external APIs, sending email, running the burst compute a client cannot. In a Zero deployment this lands naturally in server mutators; in a log-truth system it becomes a consumer that reacts to synced events.

There is also a job the 2019 essay did not dwell on that I would budget for explicitly: the cross-user read path. Once data is partitioned per user for sync-scope reasons, the admin console and the aggregate analytics query stop being a WHERE clause on one database and become a separate server-side read model, fed from the relay. The client-owned copies are the truth, but no client holds all of them; anyone who needs the global view is back to building a projection on the server.

And history grows without bound. A replica that has been offline for six months must still be able to sync, which means the log cannot be safely truncated — the 2019 essay named unbounded change history as a performance problem, and an event-sourced client store inherits it directly. Storage for the log, and time for a fresh device's initial sync, are both budget lines that a server-authoritative system simply does not have.

The dividing line: writes you can merge and writes you must refuse

Everything above is mechanics. The decision about whether an application can be local-first at all comes down to a property of its business rules, and I have not found a cleaner way to state it than this: some rules define a correct merge, and some rules define a refusal.

A convergent rule says any interleaving of concurrent writes has an acceptable combined outcome. Two people edit a document; both edits belong in the result. A checklist gets an item from each phone; the merged list holds both. CRDTs industrialize exactly this: the Ink & Switch team reported that in their Automerge prototypes the default merge semantics were sufficient in every application they built, with the only unresolvable case being concurrent assignment to the same property of the same object — and even there the structure preserves both values and lets the application choose. Merge handles it, no coordinator required.

A refusal rule is different in kind. One username per handle. One passenger in seat 14C. A balance that must not go below zero. Stock that must not be sold twice. Correctness is defined by one of the two concurrent writes losing — being rejected before it becomes durable. A merge function cannot express this: it is total, it must produce a result from both inputs, and it has no way to say no. Refusal requires an authority that observes writes in one order and can bounce one of them while the writer is still there to hear it. That is what the server-authoritative write path is.

This is not a gap that better CRDTs are about to close; it is being attacked from the other direction. The "Consistent Local-First Software" work published in IEEE TSE and presented at FSE 2025 identifies the inability to enforce application invariants across replicas as the key blocker for local-first databases, and its remedy is targeted reintroduction of synchronization for precisely the invariant-bearing operations, letting everything else stay coordination-free. Which is, once you squint, the hybrid architecture stated as a research result. Kleppmann drew the same boundary in his keynote from the practitioner side: local-first fits file editing and productivity tools, and does not fit systems managing physical resources — his example was bank accounts — where a centralized authoritative source remains the appropriate design.

So the concrete artifact I would produce before adopting any sync engine is a table with one row per collection in the data model and one column: merge or refuse. Documents, drafts, preferences, presence, annotations — merge. Money, inventory, seats, usernames, quotas — refuse. The merge rows can move to client-owned truth and gain offline writes, instant reads, and provider independence. The refuse rows keep a server-owned write path — possibly wrapped in Zero-style optimism so the UI stays instant, but with the server's word final. Most applications that demo as "local-first" ship exactly this split; the demos just never show the billing page.

The apps this breaks for

Stating the failure class directly, because the pattern's advocates too rarely do:

  • Adversarial state. If a user benefits from forging state — payments, marketplace listings, rankings, usage quotas — the client cannot hold the authoritative copy, because the authoritative copy is precisely the thing the user must not control. Zero's two-sided mutators exist for this reason.
  • Contended physical resources. Seats, stock, funds, appointment slots: refusal rules dominate the data model, and a local-first version is a double-booking generator with excellent latency.
  • Aggregate-first products. If the product is the global view — analytics, moderation consoles, fleet dashboards — client-owned partitions force you to rebuild a server-side projection anyway, and the demotion bought little.
  • Short-session, big-data surfaces. A checkout page or a support portal visited twice a year should not pay a replica's initial sync; sync engines earn their cost on data a user returns to daily.
  • Compliance-driven deletion. Erasure obligations sit uneasily with immutable logs and peers holding replicas that cannot be revoked; if legal deletion is a hard requirement, the boundary of what syncs must be drawn around it from day one.

Where the inversion pays for itself is the workspace class of software: documents, notes, design canvases, planning boards, anything a person or small group returns to across devices and network conditions. There the merge column covers nearly the whole data model, reads are interaction-critical, offline windows are real, and provider exit is a feature users can be sold.

What I would decide before adopting a sync engine

  • Classify every collection as merge or refuse before evaluating any vendor. The classification, not the vendor, is the architecture.
  • Place authority per collection: server truth with synced reads (Electric-style), server truth with speculative client writes (Zero-style), or client truth with a relay (LiveStore/Automerge-style). Mixing placements in one product is normal, not a smell.
  • Keep every refusal-bearing invariant behind a server-side write path, even inside an otherwise local-first application.
  • Treat permissions as partition design at the sync boundary, and accept that revocation of already-synced data is off the table.
  • Budget for the log: unbounded history growth, initial-sync time on new devices, and a server-side read model for anything cross-user.

The recap I keep for myself: reach for client-owned state when the data is a workspace someone returns to — collaborative, multi-device, offline-prone, merge-friendly — and the exit-from-provider property has product value. Avoid it wherever a write must be refusable: money, inventory, identity, anything adversarial or contended. And when a product needs both, that is not a compromise of local-first — per-collection authority is the architecture the research and the shipping engines have both converged on.

Further reading

Read next

Still here? You might enjoy this.

Nothing close enough — try a different angle?

Was this helpful?

Leave a rating or a quick note — it helps me improve.