39 views 23 mins 0 comments

Local‑First Apps That Keep Working: CRDTs, Fast Sync, and Simple Patterns You Can Ship

In It's happening, Technology
October 23, 2025
Local‑First Apps That Keep Working: CRDTs, Fast Sync, and Simple Patterns You Can Ship

Fast apps win. Apps that keep working on a subway, in a metal warehouse, or in a crowded stadium win even more. That is the promise of local‑first software: your data is on your device, edits are instant, and sync happens in the background without drama. The good news is that you can build this today. The tools are mature, the patterns are clear, and the tradeoffs are better understood than ever.

This article is a hands‑on tour of practical local‑first design. We will look at how to choose and apply CRDTs, store data safely, sync quickly, design a friendly UX for offline work, and ship a feature without rewriting your whole backend. Whether you are building a collaborative editor, a field tool for teams, or a personal notes app, you will find a path you can implement now.

What “local‑first” means in practice

Local‑first is often confused with “offline‑only.” That’s not the goal. The goal is to make local speed and availability the default, while cloud sync and collaboration happen continuously in the background. In practice, it usually means four things.

Data lives with the user

The canonical copy of a user’s work exists on their devices, not only on a server. Cloud storage acts as a backup and a relay to other devices. This makes apps feel fast and helps with privacy: users keep control of their information.

Sync is a background service, not a blocking step

Users never wait for a spinner to save. The app accepts edits instantly and queues changes for sync. If the network is flaky, work continues. When the network returns, changes merge.

Conflicts are resolved by design

You do not throw merge dialogs at users. You pick data structures and rules that converge without surprises. This is where CRDTs shine: they define how data merges even if edits arrive in any order.

Multi‑device is a first‑class requirement

People move fluidly between phone, laptop, and tablet. A local‑first app makes that shift seamless. Data syncs reliably, and each device has enough local state to be useful on its own.

The CRDT toolbox in plain language

CRDTs—Conflict‑free Replicated Data Types—are data structures that merge without conflicts across devices. They do it with rules that guarantee the same result no matter the order of events, packet loss, or reconnect timing. They remove the need for “last writer wins” hacks or manual merge UI for most edits. You do not have to be a theorist to use them, but you should know the main shapes so you pick the right one.

Common CRDTs and when to use them

  • Registers: Store a single value. A last‑writer‑wins register (LWW) is fine for things like a profile picture URL. Pick a clear rule for tie‑breaks, such as timestamp plus device ID.
  • Counters: For counts that may increment and decrement from many devices. A PN‑Counter tracks positive and negative changes and merges them.
  • Sets: A grow‑only set never forgets, so use it for “ever seen” lists. An observed‑remove set (OR‑Set) allows adds and removes without re‑introducing stale items during merges.
  • Maps: Key‑value stores that hold nested CRDTs. Useful for documents with fields. You can store sets, counters, or registers inside.
  • Lists and Text Sequences: Collaborative text and ordered lists need sequence CRDTs. Structures like RGA or Logoot underpin libraries such as Yjs and Automerge.

Picking the right CRDT

Think about the unit of collaboration. If a shopping list is edited by many people, use an OR‑Set for items, not a single string blob. If users edit paragraphs independently, store each paragraph as a sequence CRDT rather than an entire document as one long sequence. This keeps merge scopes small.

Causality and clocks

CRDTs often maintain a notion of causality to order events. Vector clocks or Lamport clocks can help track “happened before” relations. In practice, your CRDT library likely handles this, but you should still record per‑device IDs and monotonic counters for troubleshooting. Do not rely on wall‑clock time for merge correctness; clocks drift and networks delay packets.

Garbage collection and compaction

CRDTs keep metadata to guarantee safe merges. Over time, that metadata grows. Plan how you will compact it. Common strategies include:

  • Snapshots: Periodically write a compact snapshot of the current state and discard old operations up to a safe checkpoint.
  • Pruning tombstones: For sets and lists, remove “tombstones” after all peers acknowledge the deletion (track acknowledgements per device or per epoch).
  • Segmented logs: Store ops in segments; once compaction runs, replace many ops with a checkpoint state for that segment.

Storage layers that won’t bite you

Local‑first apps are only as good as the storage layer. You want reliability, speed, and the ability to persist large blobs like images or audio. Pick the storage your platform handles well, and keep it simple.

On the web

  • IndexedDB: The workhorse for structured data in browsers. It is asynchronous, transactional, and widely supported. Wrap it with a small utility to avoid callback sprawl.
  • OPFS (Origin Private File System): Great for large files and for running SQLite in WebAssembly. It feels like a private file system per origin and persists across sessions.
  • Service Workers: Cache assets and queue sync tasks. Be honest about limitations: periodic background sync support is uneven across browsers. Rely on “sync on open” and “sync on visibility change” as your baseline.

On mobile

  • SQLite: Use a mature wrapper (Room on Android, GRDB on iOS) for schema management and migrations. For CRDTs, keep an ops table alongside a materialized view for fast queries.
  • Filesystem: Store large attachments as files and reference them from your DB. Stream uploads and downloads to avoid memory spikes.
  • Key storage: Use the OS keystore/Keychain for cryptographic keys and tokens. Never roll your own crypto.

On desktop

LevelDB, RocksDB, or SQLite all work. Desktop apps often have more disk and memory, so you can store longer operation histories between compactions. Treat cross‑platform consistency as a priority if you ship on macOS, Windows, and Linux.

Encryption at rest

Encrypt local data where possible. On mobile and desktop, tie encryption keys to the OS keystore. In browsers, use Web Crypto to encrypt sensitive fields before writing to IndexedDB or OPFS. Rotate keys per “space” (a project, notebook, or team) so you can revoke access selectively later.

Sync protocols that scale from hobby to startup

Sync is where simple apps turn into great apps. A good protocol is small, predictable, and testable. You do not need a heavy real‑time backend to start. You can begin with a dumb relay and grow from there.

Start with push/pull and deltas

  • Push: When the app has new ops, send them to the server tagged by device ID and logical time.
  • Pull: Ask the server for ops after your last checkpoint. Batch results by document or “space.”
  • Deltas: Avoid resending full state. Send only new operations since the last sync.

Keep your server a mailbox: it stores per‑space operation logs, enforces access control, and serves deltas. All merge happens on clients. This reduces server complexity and cost.

“Sync buckets” for predictable performance

Group related documents into sync buckets—small collections that share a log and checkpoint. A bucket might be a project, a chat, or a notebook. Buckets keep state small on mobile, enable granular sharing, and make compaction simpler.

Event streams for near real‑time

For live collaboration, add a push channel: WebSocket or Server‑Sent Events. Send the same operations you already log for deltas. Avoid special “real‑time only” messages; they complicate recovery. If a device disconnects, it should resume from the log without gaps.

Access control that matches sharing

A clean pattern is to encrypt each bucket with a symmetric key. Share that key using users’ public keys (asymmetric “envelopes”). Invites can be QR codes, links, or peer‑to‑peer exchanges. When a member leaves, rotate the bucket key and re‑encrypt the latest snapshot. Old members keep what they already downloaded; they cannot decrypt future edits.

Schema evolution without drama

Design for change. Include a schema version in each bucket. When you add fields, avoid destructive renames. Allow unknown fields to pass through. Write migration code that transforms state on read into the newest shape and writes back in the new format when saving.

Designing UX for offline‑by‑default

Local‑first UX is not about surfacing sync internals. It is about making the app feel stable and respectful under all conditions.

Helpful, quiet status

  • Use small, consistent cues: a checkmark for saved locally, a dot for pending sync, and a cloud icon for cloud backup complete.
  • Do not block editing with banners. Show unobtrusive toasts when the network drops or returns.
  • Let users retry or “sync now,” but also handle it automatically.

Predictable merges

Never ask users to read a diff unless they are experts. If two people edit the same field, be explicit about rules. For example: “If we both rename a list item, the later edit by timestamp wins, but additions and reorders merge.” Explain this in settings or a help tooltip.

Device awareness

Allow users to see their signed‑in devices and revoke any. Show last sync time per device. Make it simple to add a device by scanning a QR code that carries an invite to the bucket key.

Testing and observability for sync and merge

It is easy to make a fast offline app that breaks on the first conflict. Good test coverage pays off here.

Property‑based tests for merges

For each CRDT type you use, write tests that generate random interleavings of operations across devices and assert that all peers converge to the same state. This catches edge cases early.

Network chaos

  • Simulate drops, duplicates, and reordering of messages.
  • Simulate long clock skew and daylight saving transitions.
  • Throttle bandwidth to 2G to test batching and backoff.

Logs you can read

Log operations with device ID, logical clock, and bucket ID. Provide a hidden “sync inspector” in the app to view the last 100 ops, pending queues, and last server checkpoint. Make it exportable for support tickets.

Cost and risk tradeoffs

Local‑first shifts costs from servers to clients. You still operate infrastructure, but it is simpler: object storage for backups, a relay for ops, and a small API for auth and billing. That can be much cheaper than transactional databases handling every keystroke. The main risks are:

  • Lost keys: If you use end‑to‑end encryption and a user loses all devices and recovery methods, data can be unrecoverable. Offer optional cloud‑encrypted backups or recovery contacts.
  • Moderation: If you host shared spaces, plan for reporting and removal workflows. End‑to‑end encryption limits server moderation; set expectations clearly.
  • Analytics: Avoid invasive logging. Prefer privacy‑preserving metrics, sampled and aggregated on device, then batch uploaded.

Four mini case studies

A field notes app for biologists

Each expedition is a sync bucket with a CRDT map for metadata and an OR‑Set of observations. Photos and audio live in files referenced by IDs. Team members add notes offline; devices sync via deltas when in range. A lead scientist exports a snapshot at the end of the week and locks the bucket.

A family shopping list

Use an OR‑Set for items and a list CRDT for ordering. Each family member’s phone holds the full list. When two people add “milk,” de‑duplicate by normalized text hash. Deletions are tombstoned and pruned after all devices have seen them. Invites are QR codes passed in the kitchen.

A design review whiteboard

Combine a sequence CRDT for sticky notes with a map of shapes, each carrying its own CRDT state. Real‑time sessions use WebSockets; offline work is common on flights. Snapshots compress op logs every 500 ops or 5 minutes, whichever comes first.

An audio journaling app

Recordings are files with hashed names and checksums. Metadata (title, markers, transcripts) lives in a map CRDT. Sync operates on chunks with resumable uploads. A “space” key encrypts both metadata and audio chunks. Background uploads resume only on Wi‑Fi by default.

Build your first local‑first feature

You do not have to convert your whole app. Start with one place where offline edits make sense and conflicts happen today.

  • Step 1: Identify the unit of collaboration. Define it as a bucket (project, notebook, list).
  • Step 2: Choose CRDT types that fit each field. Split text into paragraphs if needed.
  • Step 3: Add a local database (IndexedDB, SQLite) and a queue for outbound ops.
  • Step 4: Implement a simple push/pull endpoint. Persist ops by bucket and device.
  • Step 5: Add basic status UI: pending, synced, error. Keep it unobtrusive.
  • Step 6: Write merge property tests and a “sync inspector” screen.
  • Step 7: Roll out to a small group. Measure time‑to‑first‑edit and sync latency.
  • Step 8: Add compaction and snapshots once logs grow past your target size.

Common mistakes and how to avoid them

  • One giant document: Storing a whole workspace as a single text CRDT makes merges heavy. Break content into smaller pieces.
  • Ignoring deletion: Deletes must be first‑class. Track tombstones and prune only after all devices confirm.
  • Blocking UI on network: Spinners that block typing defeat the purpose. Always accept local edits.
  • Wall‑clock merges: Timestamps drift. Use library‑provided causality or monotonic counters for correctness.
  • Binary blobs in CRDTs: Do not inline large files as CRDT ops. Store them separately and reference by content hash.
  • Unbounded logs: Plan compaction from day one. Even a simple “snapshot every N ops” helps.

Security and privacy checklist

  • End‑to‑end encryption per bucket: Encrypt content with a symmetric key. Share it by wrapping with members’ public keys.
  • Key rotation: Support rotating bucket keys when membership changes. Re‑encrypt snapshots with the new key.
  • Recovery: Offer optional encrypted backups of keys, plus recovery codes. Explain tradeoffs clearly.
  • Device posture: Bind keys to hardware keystores. Require a device passcode/biometrics to unlock.
  • Secure transport: Use TLS everywhere. Prefer short‑lived tokens. Replay‑proof your push endpoints.

What to use: libraries and building blocks

You can assemble local‑first apps with a few well‑supported libraries. Pick one or two to pilot; you do not need them all.

  • Automerge: A CRDT engine with a straightforward JSON‑like API, designed for offline collaboration.
  • Yjs: A fast CRDT framework for text and structured data, with rich bindings for editors and transport adapters.
  • Replicache: A client‑side cache and sync engine for web apps with optimistic UI and server reconciliation.
  • PouchDB + CouchDB: A classic pair for document sync with revisions and conflict handling. Mature and battle‑tested.
  • CR‑SQLite: Brings CRDT replication to SQLite, making it easier to build local‑first apps on mobile and desktop.
  • ElectricSQL: Sync Postgres to local SQLite, enabling robust offline read/write with conflict resolution strategies.
  • Gun: A decentralized graph database you can run in browsers and Node, useful for peer‑to‑peer scenarios.

Whichever you choose, apply the same principles: small buckets, delta sync, predictable merges, and a UX that never punishes offline use.

Where this is heading

Local‑first design is not a nostalgic throwback. It is a modern response to network variability, privacy concerns, and cost pressure. Devices are powerful. Browsers can run full databases with OPFS. Mobile platforms expose fast storage and secure key stores. Users expect apps to be snappy and trustworthy. By making local the default and cloud a helper, you can meet all three needs at once: speed, privacy, and reliability.

Start small. Pick one feature that benefits from instant edits and resilient sync. Ship it, observe it, and iterate. You will likely find that the rest of your app wants to follow.

Summary:

  • Local‑first apps keep data on devices, sync in the background, and resolve conflicts by design.
  • CRDTs provide mergeable data types: registers, counters, sets, maps, and text sequences.
  • Use IndexedDB/OPFS on the web and SQLite on mobile/desktop; encrypt at rest with OS keystores.
  • Build a simple push/pull delta sync, grouped into “sync buckets” for performance and sharing.
  • Design quiet, helpful UI: instant edits, clear status, no blocking spinners or merge prompts.
  • Test merges with property‑based tests, simulate network chaos, and ship a sync inspector.
  • Plan for compaction, deletion handling, key rotation, and recovery from lost devices.
  • Adopt proven libraries like Automerge, Yjs, Replicache, PouchDB, CR‑SQLite, or ElectricSQL.

External References: