The offline-first conversation almost always starts in the wrong place. Someone asks which sync engine to use — CRDTs, a replication library, one of the local-first platforms — and the team spends two weeks evaluating tools.
The tool is the last decision, not the first. The one that determines whether the project succeeds is this:
When two devices change the same thing while disconnected, what is the correct outcome?
That is a question about your domain, and no library can answer it for you. Answer it first and the tool choice becomes obvious. Answer it after, and you will be fighting a sync engine that is faithfully implementing semantics you never chose.
Why “last write wins” is a decision, not a default
Most systems get last-write-wins by accident: two updates arrive, the later timestamp survives, the earlier one vanishes without a trace.
Sometimes that is correct. A user toggling a preference on their laptop and again on their phone — the last one is what they meant.
Sometimes it is a serious bug wearing a default’s clothing. A field technician updates a job’s notes offline. The office updates the same job’s status. Both sync. One update silently disappears, and nobody finds out until the job is done wrong. That is not a sync failure — every component worked as designed. It is a modelling failure: two edits to different aspects of a record were treated as competing edits to the record.
The first fix is often just granularity: sync at the field level rather than the record level and those two edits stop colliding at all. That is free, and it eliminates a surprising share of real-world conflicts.
The reframing that solves most of the rest
Here is the idea worth taking away from this article: store operations, not states.
Consider stock levels. Two warehouse staff, both offline, both counting.
- As state: device A says
quantity = 5, device B saysquantity = 3. These conflict. Whatever you do — pick one, ask a human, take the max — you are guessing. - As operations: device A says
decrement by 2, device B saysdecrement by 4. These do not conflict. Apply both. The answer is arithmetic, not arbitration.
Same scenario, same devices, same network. The difference is entirely in what you chose to record. State-based models manufacture conflicts that operation-based models never have.
This has a real cost — an operation log is more to store, more to reason about, and needs compaction — so it is not free and it is not universal. But when you find yourself designing an elaborate conflict-resolution UI, that is usually the signal you modelled states where you should have modelled intents.
Where CRDTs help, and where they mislead
CRDTs guarantee that replicas converge to the same value without coordination. That guarantee is real and valuable, and it is narrower than people assume.
Convergence is not correctness. A last-write-wins register CRDT converges — on one of the two values, discarding the other. Everyone agrees on the answer. The answer is still wrong for the stock-count case. A counter CRDT would give you the right answer, because a counter encodes the operation rather than the state.
So the useful framing is that choosing a CRDT type is choosing your domain semantics. A set, a counter, a register, and a sequence produce different answers to the same sequence of edits. Get that mapping right and CRDTs are excellent. Reach for “a CRDT” generically and you have chosen semantics by accident.
Text is where they genuinely shine — concurrent editing of a document is the problem they were built for, and hand-rolling it is a mistake. Business records with invariants (“stock cannot go negative,” “only one primary contact”) are where they fit worst, because CRDTs cannot enforce cross-object invariants. Something still has to be the authority.
The plumbing that always comes up
Four things every offline-first system needs, regardless of tooling.
Client-generated IDs. A device that is offline must be able to create records. Server autoincrement is unavailable, so IDs come from the client — UUIDs, or ULIDs if you want them sortable by creation time. Retrofitting this later means rewriting every foreign key you have.
Logical clocks, not device clocks. Device clocks are wrong. They drift, they get set manually, they jump across time zones, and users change them to cheat games. Any ordering that depends on wall-clock time will eventually order events wrongly. Lamport or hybrid logical clocks give you causality that survives a device set to 1998.
Tombstones for deletes. Deleting a row locally and syncing “it’s gone” is indistinguishable from “I never had it.” You need an explicit deletion marker, and a policy for eventually collecting those markers — which requires knowing how long a client may plausibly be offline.
Server-side revalidation. The client is not trustworthy. It may be months out of date, running an old build, or modified. Every operation gets re-checked server-side against current permissions and invariants. Optimistic UI is a presentation strategy, not an authorisation one.
The two problems that hit later
Schema version skew. A device offline for six months comes back with operations shaped for a schema you have since changed twice. This is the hardest genuinely-hard problem in offline-first, and the answer is boring: version every operation, keep migration paths for as far back as you support, and decide explicitly how old is too old — then handle that case gracefully instead of crashing.
The related discipline is keeping the sync contract narrow, for exactly the reasons a data access layer should hide the specifics of its backing store. Every field you sync is a field you must migrate across every version still in the field.
Rejection UX. The server rejects an operation the user performed twenty minutes ago and has long since forgotten. An error toast is useless — the context is gone. What actually works is showing the affected item with its previous state and what happened, in a place the user can find later. Design this deliberately; it is where offline-first apps feel broken even when the sync layer is perfect.
Decide these before you evaluate a single tool
- What is the unit of conflict? Record, field, or operation.
- For each mutable field, what is the right merge? Last write, additive, union, or a human decision.
- What invariants must hold server-side no matter what a client claims?
- How long may a device be offline? An hour changes nothing. Six months changes everything.
- Who sees a conflict when one is genuinely unresolvable — the user, or an operator?
Answer those and the shortlist collapses on its own. A team that needs collaborative text lands on a CRDT library. A team with additive counters and field-level merges usually needs far less machinery than they feared. A team that cannot answer question 3 is not ready to pick anything.
This is upstream architecture work — the kind that is cheap now and enormously expensive to revisit once devices in the field hold data in the old shape, which is why it belongs in the architecture conversation rather than in the sprint where someone integrates a library. It is also why offline capability is one of the few requirements genuinely worth designing desktop and field applications around from day one, rather than adding when the first customer complains about site coverage.
If you are staring at a sync library shortlist and have not yet answered the five questions above, that ordering is the problem — start there instead.