Design the mobile client for a real-time collaborative app (like Google Docs)
A senior deep-dive into CRDT-based mobile sync — local-first editing, offline queues, state-vector reconciliation, and why this problem looks nothing like a normal REST client.
LEARN BY DOINGPut your intuition to work3 steps
Design the mobile client for a real-time collaborative app (like Google Docs)
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Clarifying the requirements
Before picking a sync algorithm, I'd nail down what real-time collaborative needs to guarantee. Do multiple people need to see each other's edits within a second or two, or is 'eventually consistent within a few seconds' fine? Does the app need to keep working — accepting edits — while the phone is in airplane mode, and merge cleanly once it reconnects? Do we need live cursors and who's-editing-what presence, or just the merged document? Is the content plain text, or does it have structure — headings, lists, embedded images — that a naive text-diff would mangle? And what's the expected document lifetime and size, because a doc that lives for years and gets edited thousands of times has very different storage requirements than a short-lived chat-like document. Mobile adds constraints a web client doesn't have to think about as hard: intermittent connectivity is the normal case, not the edge case, and the app gets backgrounded and killed far more aggressively than a browser tab.
#OT vs CRDT — the decision that shapes the whole client
There are two well-established families of algorithms for this problem, and the choice between them decides almost everything downstream. Operational Transformation — the model Google Docs itself popularized — keeps a single server as the arbiter of order: every client sends operations to the server, which transforms each incoming operation against whatever concurrent operations it already applied, and broadcasts the transformed result to everyone else. It works well, but it fundamentally requires a live, reachable server to make sense of concurrent edits, which is an awkward fit for a mobile client that needs to keep accepting edits with no connection at all. CRDTs — Conflict-free Replicated Data Types, the model behind libraries like Yjs and Automerge — take the opposite approach: every operation is designed so that applying it in any order, on any replica, converges to the same result without a central authority resolving anything. That makes CRDTs the natural fit for a mobile client, because the phone can keep being a fully functional replica of the document even with zero connectivity, and reconciliation on reconnect is a mechanical merge rather than a negotiation with a server.
#The mobile client as a local-first CRDT store
Given that choice, I'd architect the phone so the CRDT document is the client's source of truth, not a cache of something the server owns. Every keystroke mutates the local CRDT document immediately, the UI renders straight from that local state — which is what gives you the instant, no-latency local echo users expect from a text editor — and the resulting operation is appended to a small durable log on disk before anything is sent anywhere, so a killed app doesn't lose the edit. A background sync layer diffs that local log against the last state it successfully sent, batches it into a compact binary update (Yjs and Automerge both have efficient binary encodings — nowhere near the size of JSON), and pushes it to the server over a persistent connection when one is available. Incoming updates from other clients flow through the exact same merge function the local edits used, so there is no separate code path for applying a remote patch to keep in sync with the local one — correctness comes from having exactly one code path for merging, ever.
#Presence and cursors: a separate, ephemeral channel
A live cursor position or a typing indicator is a completely different kind of data from the document itself — it's high-frequency, has no value once it's stale, and should never be persisted or fought over in a merge. I'd keep it entirely out of the CRDT document and run it over a separate awareness channel (the same pattern Yjs calls awareness): each client periodically broadcasts its own ephemeral state — cursor position, selection range, a user color — over the live connection, other clients hold the latest broadcast per user in memory, and if a client disconnects its presence simply expires after a timeout. Throttling those broadcasts (a few times a second, not on every pixel of cursor movement) matters more on mobile than on web, since it's both a battery and a data-plan cost that a wall-mounted desktop client doesn't have to worry about as much.
#Offline-first and background sync
The fact that local writes always succeed is the whole point, but I still want the document to be reasonably fresh the next time the user opens it, even if they never explicitly reopened the app to sync. On iOS I'd register a BGTaskScheduler task to attempt a short, best-effort sync while the app is suspended; on Android, WorkManager with network constraints does the same job with a stronger delivery guarantee. Either way, the merge itself should never run on the main thread — a large document with a long edit history can mean a non-trivial CRDT merge, and doing that inline with the UI thread is a guaranteed source of dropped frames right when the user is mid-keystroke. I'd push merges to a background queue and only marshal the rendered result back to the UI thread.
#Keeping the document from growing forever
That tombstone growth is the single biggest long-term cost of the CRDT approach, and a senior answer should name the mitigation up front rather than waiting to hit it in production. The standard fix is periodic compaction on the server: once every connected client has acknowledged a given point in history, tombstones and superseded operations before that point can be garbage-collected, and the document can be snapshotted so a new client joining doesn't have to replay the entire operation history from time zero — it loads the snapshot and only streams operations since it was taken. I'd also cap how far back a single client can lag before the server insists on a full snapshot resync instead of an incremental delta, so a phone that's been offline for three months doesn't reconstruct three months of individual keystrokes.
#Tradeoffs I'd surface
CRDTs trade server simplicity and offline resilience for metadata overhead and eventual, rather than server-enforced, ordering — which is the right trade for a mobile-first product but the wrong one if the product's real requirement is a strict, centrally-arbitrated order (financial ledgers, for instance, want OT-style or even stronger consistency, not CRDT convergence). Structural conflicts still need product-level decisions the algorithm can't make for you — if two people concurrently delete the same paragraph one of them is still editing, the CRDT guarantees the document doesn't corrupt, but it doesn't guarantee the result feels sensible to either user, so the UI layer needs its own affordance for surfacing that someone else changed this while you were typing. And choosing a mature library (Yjs, Automerge) over a hand-rolled CRDT is almost always the right call — the edge cases in concurrent rich-text merging are the kind of thing that takes years to get right, and this is a case where reinventing the wheel is a genuine liability, not just wasted effort.