What we thought offline-first meant
First instinct: keep a local cache, write to it freely, sync to the server when the connection comes back. Sounds reasonable. Survives a demo. Falls apart at the first conflict.
V1, last-write-wins
The first version was last-write-wins. A coach edits a client's program at 2pm, the trainee logs a session at 2:01pm while the coach is still offline, both writes hit the server when the coach reconnects. The coach's edit wins, the trainee's log is gone.
We lost trainee data. Not a lot. Enough. Two weeks after launch I was on a call with a coach explaining why their client's logged sets had vanished. That is not a conversation you have twice.
V2, per-doc CRDT
The second version implemented per-document CRDTs (conflict-free replicated data types). Mathematically clean. Every conflict resolved deterministically. No data loss.
It also slowed the app down by about 40% on the affected screens. CRDT bookkeeping is not free, especially on a phone with a year-old battery and a constrained CPU. We had traded data loss for a perceptibly slower app, which trainees noticed within 3 days.
V3, queue plus idempotency plus server resolve
The current version: every write goes into a local queue with an idempotency key. The queue drains to the server in order. The server holds the canonical state and resolves conflicts using domain rules (trainee logs win on session-level writes, coach edits win on program-level writes, last-write-wins on settings).
The client never has to reason about conflicts. The server does. The client just queues and replays. Performance is back to where it was in V1. Data loss is back to where it was in V2. The win came from picking the right place to put the complexity.
“Offline-first is a queue, an idempotency key, and a server that resolves. Anything more clever falls apart.”Moe Talaat, Teshape
What I would do again
I would build V3 from day one. The intermediate versions were necessary to learn what V3 needed to look like, but the lesson cost real users real data. If you are building an offline-first app today, start with a queue plus idempotency keys. The CRDT temptation looks academic-tier elegant and is operationally painful.



