Offline-First Sync
Introduction
The last lesson ended in an airport lounge: a phone, a week offline, a queue of edits, and a sync about to happen. Every strategy you now own assumed the conflict was already found. This lesson is the machine that finds it — and it starts by renaming the situation.
A phone is not a client that occasionally loses its connection. It is a partition with a battery: tunnels, flights, dead spots, and a radio the operating system turns off to save power. Offline is not an edge case to handle; it's the default state of a pocket computer. In 2019, Martin Kleppmann and the Ink & Switch lab gave the resulting architecture its name in a manifesto titled Local-first software: you own your data, in spite of the cloud: the app reads and writes its own local store, instantly, always — and the network, when it appears, is an optimization. Every tap lands in milliseconds because it never leaves the device. What remains is the loop that reconciles your store with everyone else's, and that loop is this lesson.

The Loop, Station by Station
Seven stations, one cycle. Walk it once slowly; you'll run it in the lab.
The local store. The UI's only source of truth. Reads never wait on a network; writes commit here first. This is why the app feels instant on a plane.
The durable queue. Every local write also appends an operation — with an id minted at birth — to a queue that survives app kills and reboots. If the phone dies mid-flight, the edits are still there at breakfast. An op that only lives in memory is an op you've already lost.
The reconnect detector. Radio returns, a notification arrives, the user opens the app: something says now. Cheap, debounced, and allowed to be wrong — a false start costs a failed request, nothing more.
Push replay. The queue drains in order, each op carrying its id as an idempotency key, the discipline from Idempotency: Safe to Retry doing exactly what it was hired for. The server applies, records the id, acknowledges.
Pull with a cursor. "Give me everything after sequence 4732." The client never asks for everything; it asks for everything since. The cursor is the loop's memory of the other side.
Converge. Remote changes meet local state, and the strategy you chose per field — last lesson's whole discipline — earns its keep: union the tags, CRDT the counter, sibling the cart, refuse the seat.
Checkpoint. Both sides record how far they got. This station looks like bookkeeping. The next section shows it's the difference between a hiccup and starting over.

Surviving the Half-Dead Sync
Now the part that separates toy sync from shipped sync. Your push is three ops deep when the elevator doors close. Op a1: sent, applied, acknowledged. Op a2: same. Op a3: sent, applied — and the acknowledgement dies in the tunnel with the connection.
What does the client know? That a3 is unacknowledged. What can it conclude? Nothing — an unacked op is applied-or-lost, and no amount of waiting distinguishes them. You've met this exact wall before: dead is indistinguishable from slow, wearing a mobile data plan. So the client does the only defensible thing: re-send everything unacknowledged. Which means the server will, sometimes, receive the same op twice — and that's why every op carries its id. Seen this id before? Drop it, re-acknowledge, move on. The counted-once law from the convergence contract, promoted from state merges to effects: a merge applied twice is harmless by algebra, but a 20 bug. Different law, same loop.
The pull side has its own half-death, and the fix is the checkpoint. This isn't theory: CouchDB's replication protocol — the same one PouchDB runs on phones — records a replication log on both source and target precisely so that a failed sync "can resume from last point of success, not from the very beginning." A week of changes, a crash at 90%, and the next attempt starts at 90%.
One more mercy: the queue can be compacted. Two hundred edits to the same field while offline don't need two hundred round trips; later ops that supersede earlier ones collapse before the push. The queue stores intent, not keystrokes.

See It, Break It: The Sync Loop Lab
Below: a phone, a server, and everything this lesson claimed, live. Go offline, add a few expenses, and watch the queue mint op ids. Reconnect, sync, and read the sequence numbers and the checkpoint advancing. Then be the elevator: kill the sync mid-flight and sync again — first with idempotency keys on, watching the server drop the duplicate and keep the total exact; then with keys off, watching the same honest re-send quietly inflate the ledger. Finish on the pull side: add an entry from the other device and confirm the loop fetches only what's newer than the checkpoint, never the whole history.

The Pocket's Rules
Four realities keep the loop honest on real devices.
The radio is expensive. Firing the loop on every keystroke murders the battery, so real clients batch: sync on reconnect, on charge, on push notification, on app-open. The queue's durability is what makes patience safe.
You don't sync the world. A phone holds the hot subset — recent notes, this month's expenses — and pulls the rest on demand. The cursor discipline works per collection, and "partial" is the only size that fits in a pocket.
Order the loop deliberately. Pull-then-push lets local ops rebase onto the freshest state; push-then-pull favors getting your writes durable first. Either is defensible; interleaving them carelessly is how carts duplicate.
Tell the user the truth. A pending badge, a synced tick, a conflict surfaced instead of swallowed. The UI is part of the protocol: an app that shows saved while ops sit in a dead queue is lying about durability, and users make real decisions on that lie.
The Bridge: Collaboration Is This Loop, Faster
Step back and look at what this section has quietly assembled. A local store, a durable queue, replay with dedup, a cursor, and a per-field merge strategy — that's not just the architecture of an offline expense app. It's the chassis under every collaborative product you use.
Google Docs is this loop running at millisecond cadence with transformed operations as the converge station. Figma is the loop plus server-ordered registers. Apple Notes is the loop at minutes, converging with self-merging types. Your shopping cart is the loop at hours, converging by union. The products feel wildly different; the machine is the same, tuned by two dials: how often the loop runs, and which strategy sits in the converge slot. Real-time collaboration is offline-first software with the offline shrunk to milliseconds — which is why this lesson, and not any single merge trick, is the real answer to "how would you design Google Docs?"

Key Takeaways
- A phone is a partition with a battery, so invert the architecture: the local store is the source of truth and the network is an optimization. That inversion has a name — local-first — and a manifesto worth reading.
- The loop has seven stations: local store, durable queue, reconnect, push replay, pull-with-cursor, converge, checkpoint. Each one exists because a specific failure exists.
- The half-dead sync is the exam: unacked is applied-or-lost, so re-send everything and dedup by op id. Counted-once, promoted from merges to effects — money is not a CRDT.
- Checkpoints turn crashes into hiccups. Resume from the last success, never from zero; the shipped replication protocols have done it this way for years.
- Collaboration is this loop at millisecond cadence. Two dials — frequency and converge strategy — span everything from a cart to Google Docs.
And that completes the working machinery of this section: conflicts chosen, types that merge themselves, operations that transform, strategies priced, and the loop that runs them from a pocket. What remains is to prove you own the judgment — four very different apps, each demanding its strategy, in Checkpoint: Collaborative & Replicated State.