13 views 22 mins 0 comments

Make Offline Collaboration Work: CRDT Choices, Storage, Sync, and UX Patterns

In Guides, Technology
February 09, 2026
Make Offline Collaboration Work: CRDT Choices, Storage, Sync, and UX Patterns

Offline‑first is no longer a novelty. Travelers expect their notes to update on planes. Field teams need checklists that work in basements. Families want shared shopping lists that don’t blink when the supermarket has no signal. If you build collaboration into your app, treating the network as a bonus makes your product steadier, faster, and friendlier.

This guide is a practical blueprint for shipping offline collaboration with conflict-free replicated data types (CRDTs). We’ll cover how to choose data types, store and compact state, sync over unreliable links, and design interfaces that stay calm when edits collide. By the end, you’ll have an approach you can actually ship, from a small team app to a full consumer product.

Where CRDTs Fit—and Where They Don’t

CRDTs are data structures that converge to the same value across devices without central coordination, even when users edit concurrently. They are great for documents, canvases, counters, lists, shared boards, and other “eventually consistent” collaboration.

You may not need CRDTs if:

  • You have a single source of truth with strict locks (for example, a financial ledger that demands serializable transactions).
  • Your edits are rare and happen on one device at a time.
  • Latency is low and permanent connectivity is guaranteed (rare in practice).

If you need strong, immediate invariants across many users (e.g., “never oversell inventory”), you’ll still enforce those on a server. Use CRDTs for the user-facing workspace. Maintain guardrails and reconciliation on the backend.

Picking the Right CRDTs

Not all CRDTs behave the same. Choose data types that match user tasks, not just the underlying math.

Core building blocks

  • Registers (LWW or multi-value): Store a single value. LWW (last-writer-wins) uses timestamps or causal order to pick the “latest.” Multi-value registers keep all concurrent values, which you can surface to users or resolve.
  • Sets (G-Set, 2P-Set, OR-Set): G-Set only adds. 2P-Set allows add/remove but can’t re-add once removed. OR-Set supports add and remove with tombstones so re-addition is safe.
  • Counters (PN-Counter): Support increments and decrements across replicas, merging without double-counting.
  • Maps (LWW-Map, OR-Map): Key-value stores where values are themselves CRDTs. Useful for documents with sections or cards on a board.
  • Sequences (RGA, LSEQ, YATA): Model ordered lists like text. They avoid index collisions by assigning stable positions to items.

Delta‑ vs operation‑based

In operation‑based CRDTs you share each user operation. In delta‑based CRDTs you share compact “diffs” that produce the same effect as many operations. Deltas can cut bandwidth on slow links. Operation‑based can be simpler to reason about during debugging. Many modern libraries let you choose or abstract this away.

Identity and causality

CRDTs must know “who did what when,” without relying on exact wall‑clock time. You’ll see vector clocks, Lamport timestamps, and per‑actor IDs. The important part: each device needs a stable identity, and edits carry enough metadata to resolve concurrent changes deterministically.

Storage That Survives Travel, Battery Swaps, and Years

Good storage is half of offline success. You want fast local reads, reliable writes, and compact history—even when a device disappears for months.

Model the state

  • Append‑only log: Store every op or delta. Ideal for time travel and debugging. Grows unbounded; you’ll need compaction.
  • Materialized state: Keep current state in a database plus periodic snapshots of the CRDT internals. Recover quickly on startup.
  • Hybrid: Keep an op log for a while, fold it into snapshots on a schedule, and garbage collect confirmed tombstones.

On mobile, SQLite gives you durability and indexing. In browsers, IndexedDB performs well for large state. On desktop, embed SQLite or use a local key‑value store. Avoid “just keep it in memory and dump to a JSON file” for production; it’s brittle under crashes and power loss.

Compaction and garbage collection

Sequence CRDTs can grow metadata (think “tombstones” for deletions). Plan a compaction cycle:

  • When all known peers have seen a deletion, drop its tombstone.
  • Snapshotted state becomes the new baseline; truncate your log up to a compacted epoch.
  • Use size or time thresholds to trigger compaction, not only a manual action.

Make compaction incremental to avoid expensive pauses. If you’re using a library, check whether it supports compaction natively and under what conditions. Don’t rewrite your entire file on every small change.

Encryption and access control

CRDTs sync peer‑to‑peer or through an untrusted relay just fine—but only if you encrypt. A common pattern:

  • Generate a document key for each shared workspace. Encrypt all state with it.
  • Wrap that key for each authorized device using that device’s public key.
  • Store wrapped keys with the document metadata. Users can add or revoke devices without re‑encrypting all content.

Sign edits to prove authenticity. If you run a relay, treat it as blind storage. Avoid server‑side decryption unless you must. This keeps “view” and “edit” access client‑enforced and auditable.

Sync Topologies That Tolerate Bad Days

CRDTs tolerate conflict; your sync layer should tolerate chaos. Build for intermittent connectivity, backpressure, and “eventual” delivery.

Client–server and peer‑to‑peer

  • Client–server: Devices sync with a relay or gateway over WebSocket, HTTPS, or gRPC. Easy to deploy and monitor. Good for access control and audit logs.
  • Peer‑to‑peer: Devices sync directly via local network or WebRTC. Great for low latency in the same room and low cost at scale. Fall back to relays for NAT traversal or when peers aren’t online.

You can mix both: peer‑to‑peer when possible, relay when necessary. CRDTs don’t mind duplicate deliveries.

Pull, push, and batching

  • Push new local deltas immediately when online. Coalesce tiny ops into batches to save battery and bandwidth.
  • Pull with backoff when idle. Use cursors or version vectors to request only what’s new.
  • Snapshot transfer for new devices. Send a compacted snapshot plus recent deltas instead of the entire history.

Background work on the web and mobile

  • On web, use Service Workers to accept pushes and run background sync. Store updates in IndexedDB and notify the page when visible.
  • On iOS/Android, batch outgoing sync to OS‑friendly windows. Respect power saver modes. Mark critical documents for higher priority.

Design for idempotence. Receiving the same delta twice should be harmless. Losing the last response chunk should not corrupt state.

UX Patterns for Eventual Consistency

Users don’t care about vector clocks. They care whether their cursor jumps, whether the checkbox stays checked, and whether someone “stole” their edit. Design your interface to stay steady and honest.

Show truth without panic

  • Optimistic edits: Apply local changes immediately. Reflect their status with subtle affordances (a dot, “saving…”, or an offline badge).
  • Plain language: “Edited by Priya 2 minutes ago” beats “Version 5f2a7… merged.”
  • Non‑blocking conflicts: If two users type in the same paragraph, let both edits land; surface a review toast with a diff and quick controls (“keep both,” “rephrase,” “undo mine”).

Presence without noise

  • Show cursors and selections sparingly. Focus on nearby or collaborating users, not a dozen avatars blinking in the distance.
  • Use throttled presence updates; prioritize content changes over cursor moves.
  • Let users mute presence in long, solo sessions.

Undo that respects others

Offer per‑user undo. In CRDT terms, that means generating compensating ops for the user’s own changes, not rewinding the whole document. If another user edited after you, your undo should not erase their work.

Explain “out of order” gently

Occasionally, an edit arrives late. CRDTs will merge it in. If that changes visible text, show a brief “updated” marker near the affected region. Don’t scroll users away from where they’re working.

Performance Without Guesswork

CRDTs have a reputation for being heavy, but careful choices keep them fast and small.

Pick scope and granularity

  • Keep documents reasonably sized. If a doc grows past a few megabytes of CRDT state, split it into sections or chapters (each a CRDT map/sequence) that sync independently.
  • Model lists as arrays of items with item content stored separately. Editing an item shouldn’t re‑emit the whole list.

Index what you need

CRDT libraries optimize merges; your app still needs queries. Build secondary indexes in your local database for search and filtering. Update indexes as deltas apply, not via full scans.

Profile device reality

  • Measure time to first usable (open app to first editable screen) and time to sync (open to “up to date”).
  • Track memory and CPU on low‑end Android and older laptops. Budget work in small chunks to stay responsive.
  • Ship a performance switchboard: flags to toggle compaction frequency, snapshot interval, and batch size for A/B tests.

Testing and Debugging the Strange Cases

You don’t have to predict every weird network story, but you should simulate the classics.

Event storms and partitions

  • Run chaos tests with artificial latency, packet reordering, and drops. Verify state convergence across 3+ replicas.
  • Simulate a device that’s offline for days, makes 1,000 edits, then reconnects. Check merge time and UI stability.

Property‑based tests

Formalize simple invariants like “list length is non‑negative,” “no duplicate IDs,” or “total = sum of counters.” Generate random operation sequences on multiple replicas and assert that invariants hold after merge.

Time travel and audit

Keep a developer toggle to show a time travel timeline: scrub through snapshots, step deltas forward and backward, and highlight UI changes. Production logs should store opaque edit IDs and device IDs for debugging, without leaking content.

Migrations, Versions, and Backward Compatibility

Live products evolve. Plan for schema changes early.

Version your CRDT state

  • Add a schema version to document metadata. When it changes, ship a deterministic migration that rewrites the CRDT value into the new shape.
  • Support reading older versions without crashing; prompt users to update before they can edit if necessary.

Immutable IDs and references

Never recycle item IDs. References across maps, sets, and sequences should point to stable identifiers. Migrators can add new fields but should leave old IDs intact.

Rolling upgrades

Deploy server and client changes gradually. If you change the delta wire format, version it and keep both readers for a transition window. Add a feature flag to fall back to older compaction rules if you discover a performance cliff.

Safety, Recovery, and Trust Boundaries

Offline means devices can be lost, reset, or stolen. Keep users safe without sacrificing autonomy.

Backups and recovery

  • Encrypt local state at rest with device keys.
  • Back up encrypted snapshots to a cloud of the user’s choice or to your relay using zero‑knowledge storage. Don’t assume your app is their only backup.
  • Offer recovery codes for the user’s identity or keychain so they can add new devices if all old ones are gone.

Revocation and shared spaces

  • If someone is removed from a shared doc, rotate the document key and re‑wrap for remaining devices. Their device will stop decrypting future edits.
  • Keep membership changes part of the document’s metadata history. Users appreciate an audit trail.

A Minimal Rollout Plan You Can Reuse

Here’s a pragmatic sequence that many teams have shipped successfully:

  1. Model the document as a CRDT map that contains a sequence for rich text and sets/maps for annotations, comments, or tasks.
  2. Persist locally with SQLite or IndexedDB. Store both materialized state and a rolling op log. Add a size cap and periodic snapshotting.
  3. Secure the data with per‑doc encryption and per‑device key wrapping. Sign edits. Treat the server as a relay and backup vault.
  4. Implement sync over WebSocket to a relay, with optional peer‑to‑peer for local sessions. Batch outgoing deltas and apply idempotently.
  5. Design UI for optimism and clarity: live edits, gentle conflict callouts, presence that can be muted, and per‑user undo.
  6. Test chaos: partition, reorder, duplicate, and throttle. Instrument convergence time, memory, and cold start.
  7. Ship gradually to a small cohort. Monitor compaction, battery impact, and error rates. Adjust batch sizes and snapshot intervals.

Concrete Examples of Data Modeling

Shared checklist

Use an OR‑Set of items with a map for properties. Mark completion with a boolean in a register. Two users can mark complete; the final state converges to “complete.” If either unchecks concurrently, you will get a deterministic outcome that depends on your chosen policy—usually last‑writer‑wins on the boolean register. If that’s not desirable, store per‑user votes in a map and compute “complete if everyone checks” or “complete if anyone checks.”

Rich text editor

Use a sequence CRDT (such as RGA‑style positions) for text. Bold/italic can be spans stored as a map from position ranges to attributes. Deletions adjust spans; merges keep them consistent. Comments are stored as a set keyed to anchoring positions; if text moves, anchor to stable positions (not raw indices) so annotations travel with the content.

Kanban board

The board is a map of lists. Each list is a sequence of card IDs. Cards are maps with fields (title, labels, dates). Reordering is sequence operations; editing details doesn’t resend the whole board. Archiving is a remove from the list sequence plus a flag on the card. Compaction can sweep archived cards after N days.

Cost and Operations Without Surprises

CRDTs shift costs from servers to clients. That’s good if you design for it.

Server role

  • Relay messages, store encrypted snapshots, maintain lightweight indexes for discovery (which documents exist, who can access them).
  • Run a queued compaction service for users who opt in to cloud backup; it can pre‑compute snapshots for cold devices.
  • Keep metrics: average payload sizes, sync frequency, late device reconnect delays.

Client budgets

  • Throttle background sync on mobile data. Respect user settings for “Wi‑Fi only” uploads.
  • Defer non‑critical compaction until on charger and idle.
  • Offer a “light mode” for low‑memory devices that limits the number of open documents in memory.

Common Pitfalls—and How to Avoid Them

  • Relying on wall‑clock time for merge decisions. Use causal metadata (Lamport or vector clocks) instead.
  • Storing everything as one giant document. Split into smaller CRDTs so changes stay local and compaction is tractable.
  • Over‑notifying users about conflicts. Silent merges are fine; reserve notifications for visible surprises.
  • Forgetting idempotence. Reapplying a delta should do nothing the second time.
  • Skipping compaction. Tombstones grow. Plan a lifecycle from day one.

What “Done” Looks Like

If you get this right, your product feels calm under stress. A subway ride doesn’t break flow. Airplane mode is just another environment. Battery holds up because you batch work. Users see truthful messages without panic. You remove a whole class of “sync bugs” from your support queue. And when the network is perfect, everything is still fast—because local-first reads are instant.

The beauty of CRDTs is that they eliminate the central paradox of collaboration: people editing at the same time don’t have to block each other or lose work. With a careful storage plan, secure keys, and a considerate UI, you can ship offline collaboration that feels modern, durable, and trustworthy.

Summary:

  • Use CRDTs for documents, boards, and lists where eventual consistency is acceptable.
  • Pick data types that match tasks: registers, sets, counters, maps, and sequences.
  • Persist with a hybrid of materialized state, op logs, snapshots, and planned compaction.
  • Encrypt per document, wrap keys per device, and sign edits; treat servers as blind relays.
  • Sync via client–server and optional peer‑to‑peer, with batching and idempotence.
  • Design calm UX: optimistic edits, gentle conflict cues, per‑user undo, and muted presence.
  • Profile on low‑end devices; index for queries; keep documents scoped and chunked.
  • Test chaos: partitions, reordering, and long offline periods with property‑based checks.
  • Version schemas, keep immutable IDs, and roll out changes with dual readers.
  • Control costs by shifting work to clients and backing up encrypted snapshots in the cloud.

External References:

/ Published posts: 199

Andy Ewing, originally from coastal Maine, is a tech writer fascinated by AI, digital ethics, and emerging science. He blends curiosity and clarity to make complex ideas accessible.