The Local-First Revolution: Mastering CRDTs and Distributed State on the Web
Back to Archive
2026-09-3028 min read

The Local-First Revolution: Mastering CRDTs and Distributed State on the Web

A comprehensive guide to transitioning from cloud-dependent architectures to local-first applications using Conflict-free Replicated Data Types (CRDTs), WebRTC, and deterministic state resolution.

For the past two decades, web development has been dominated by a singular architectural paradigm: the cloud is the source of truth, and the client is merely a highly functional terminal. We build complex single-page applications (SPAs), yet the moment the network drops—whether you are on a subway, a plane, or a spotty cellular connection—the application degrades into a paralyzed state of loading spinners and "Network Error" toast notifications.

The latency between a user clicking a button and a database in us-east-1 registering that action is governed by the speed of light and the inefficiency of network routing. No amount of React optimization can fix a 300ms round-trip time.

Enter the Local-First Architecture.

In a local-first application, the local database is the primary source of truth. Reads and writes happen synchronously against a local data store, resulting in 00ms network latency. The application then synchronizes this state with other peers and cloud backups asynchronously in the background.

But this introduces a massive computer science problem: If two users modify the same data locally while disconnected, how do we merge those changes without conflicts or data loss when they come back online?

The answer lies in Conflict-free Replicated Data Types (CRDTs).


The CAP Theorem and the Fallacy of Consensus

To understand why traditional databases fail in offline scenarios, we must look at the CAP Theorem, which states that a distributed data store can only simultaneously provide two of the following three guarantees:

  1. Consistency (C): Every read receives the most recent write or an error.
  2. Availability (A): Every request receives a non-error response.
  3. Partition Tolerance (P): The system continues to operate despite an arbitrary number of messages being dropped by the network.

When a user's phone goes into airplane mode, a network partition (P) has occurred. Traditional SQL databases prioritize Consistency (CP). Therefore, when partitioned, they must sacrifice Availability—they lock the database and refuse writes to prevent conflicts.

Local-first architectures must prioritize Availability and Partition Tolerance (AP). Users must be able to write data offline. This means we are forced into Eventual Consistency.

Historically, eventual consistency was handled via Operational Transformation (OT)—the algorithm powering Google Docs. However, OT requires a central server to sequence operations. If the server is unreachable, collaboration halts. CRDTs solve this by eliminating the need for a central sequencer entirely.


The Mathematics of CRDTs

A CRDT is a data structure designed such that any two instances can be updated independently, and their states can always be merged back together deterministically, without requiring coordination.

There are two main families of CRDTs:

  1. CvRDTs (State-based / Convergent): The entire state is shipped across the network and merged.
  2. CmRDTs (Operation-based / Commutative): Only the specific update operations are broadcasted.

The Join Semi-Lattice

The magic of state-based CRDTs is rooted in abstract algebra. For a state SS to be a valid CvRDT, its merge function (let's call it the join operator, denoted as ∨\lor) must form a Bounded Join Semi-Lattice.

Mathematically, a join semi-lattice is a partially ordered set (S,≤)(S, \le) equipped with a binary operation ∨\lor that satisfies three non-negotiable properties:

  1. Commutativity: The order in which states are merged does not matter. a∨b=b∨aa \lor b = b \lor a
  2. Associativity: Groupings of merges do not matter. (a∨b)∨c=a∨(b∨c)(a \lor b) \lor c = a \lor (b \lor c)
  3. Idempotence: Merging the same state multiple times has no additional effect. a∨a=aa \lor a = a

Because of these three properties, the network can drop packets, duplicate packets, or deliver packets out of order, and all peers are still mathematically guaranteed to arrive at the exact same final state once all messages are received.


Building a Practical CRDT: The LWW-Map

Let's implement a real-world CRDT. A standard JSON object or JavaScript Map cannot merge conflicts deterministically. If Alice sets doc.title = "A" and Bob sets doc.title = "B" while offline, who wins when they sync?

We can solve this with a Last-Writer-Wins Map (LWW-Map). To make it deterministic, every value must be accompanied by a logical timestamp (or a Lamport clock) and a unique replica ID to break ties if timestamps are identical.

type ReplicaID = string;
type Timestamp = number;

interface LWWRegister<T> {
  value: T;
  timestamp: Timestamp;
  peerId: ReplicaID;
}

export class LWWMap<K, V> {
  private state: Map<K, LWWRegister<V>>;
  private readonly peerId: ReplicaID;

  constructor(peerId: ReplicaID) {
    this.state = new Map();
    this.peerId = peerId;
  }

  // Write operation
  set(key: K, value: V, timestamp: Timestamp = Date.now()): void {
    const existing = this.state.get(key);
    
    // Only update if the new timestamp is strictly greater,
    // or if timestamps are equal, fallback to lexicographical peerId sorting
    if (
      !existing ||
      timestamp > existing.timestamp ||
      (timestamp === existing.timestamp && this.peerId > existing.peerId)
    ) {
      this.state.set(key, { value, timestamp, peerId: this.peerId });
    }
  }

  // Read operation
  get(key: K): V | undefined {
    return this.state.get(key)?.value;
  }

  // The Join function (a \lor b)
  merge(other: LWWMap<K, V>): void {
    for (const [key, remoteRegister] of other.state.entries()) {
      const localRegister = this.state.get(key);

      if (
        !localRegister ||
        remoteRegister.timestamp > localRegister.timestamp ||
        (remoteRegister.timestamp === localRegister.timestamp &&
          remoteRegister.peerId > localRegister.peerId)
      ) {
        this.state.set(key, remoteRegister);
      }
    }
  }

  getState(): Map<K, LWWRegister<V>> {
    return new Map(this.state);
  }
}

Notice the merge function. It strictly obeys Commutativity, Associativity, and Idempotence. Alice can merge Bob's map, and Bob can merge Alice's map, and they will mathematically converge to the same internal state.


Collaborative Text and Sequence CRDTs

While an LWW-Map is great for key-value data (like setting a user's profile name), it fails disastrously for sequential data like collaborative text editing.

If Alice inserts a word at index 5, and Bob deletes a word at index 3, Bob's deletion shifts the indices of all subsequent characters. If we just blindly exchange index-based operations, Alice's insertion will land in the wrong place.

Fractional Indexing and Tombstones

Sequence CRDTs (like Replicated Growable Arrays - RGA) solve this by assigning fractional, globally unique identifiers to every single character, rather than relying on their array index.

Instead of saying "Insert 'A' at index 5", a Sequence CRDT says: "Insert 'A' between character ID 0.45 and character ID 0.46".

If we need to insert something between 0.45 and 0.46, we simply generate a new ID halfway between them (e.g., 0.455). Because these IDs are mathematically continuous (similar to real numbers), we never run out of space between any two characters.

Furthermore, we never actually delete characters from memory. Deleting a character simply marks it with a Tombstone (a boolean flag indicating it is deleted). This ensures that historical references remain intact for late-arriving offline operations.

interface CharRegister {
  id: string; // Fractional Index + PeerID
  value: string;
  isDeleted: boolean; // Tombstone
}
// When rendering the text, we simply filter out tombstones:
// const visibleText = state.filter(char => !char.isDeleted).map(char => char.value).join('');

Libraries like Yjs and Automerge implement highly optimized versions of these algorithms. They compress the tombstones and operation logs so efficiently that you can store the entire editing history of a massive document in just a few megabytes.


The Storage and Network Topologies

1. The Local Storage Layer (OPFS)

For local-first to work, the browser must have a robust, high-performance database. localStorage is synchronous and blocks the main thread, making it unusable for large data. IndexedDB is asynchronous but notoriously clunky and slow for complex queries.

The modern solution is OPFS (Origin Private File System) combined with WebAssembly (Wasm). Today, we compile the entirety of SQLite into Wasm and mount it to the OPFS. This grants the browser a literal, file-system-backed, highly performant SQL database that runs locally, processing thousands of transactions per second without touching a network stack.

2. The Synchronization Topology

Once data is saved locally, how does it move? Local-first allows for hybrid network topologies:

  • Peer-to-Peer (WebRTC): If Alice and Bob are in the same coffee shop, their devices can discover each other via WebRTC data channels and sync CRDT states directly over the local network, even if the coffee shop's external internet connection goes down.
  • Relay Servers (WebSockets): For standard internet collaboration, a dumb relay server is used. Unlike traditional servers, the relay does not parse, authorize, or resolve data conflicts. It simply acts as a pub/sub router, taking an encrypted binary CRDT update from Alice and broadcasting it to Bob.
  • Cloud Backup: A cloud node acts merely as another "peer" in the network, silently maintaining a durable backup of the state.

Security: End-to-End Encryption by Default

Because the server no longer handles state resolution, the server does not need to read the data. This makes local-first architectures inherently perfectly suited for End-to-End Encryption (E2EE).

When Alice generates a CRDT update, she encrypts the binary payload using a shared symmetric key derived from a protocol like the Signal Protocol. The payload is sent to the relay server. The relay server sees only cipher-text. It broadcasts it to Bob. Bob decrypts the payload locally and feeds it into his local CRDT.

The result is a collaborative, real-time application where the infrastructure provider has absolutely zero knowledge of the data passing through their systems.


Conclusion: Refactoring the Web

Transitioning to local-first is not just changing a database; it is a fundamental unlearning of stateless request/response paradigms. We are returning to the stateful programming models of desktop applications, but augmenting them with distributed systems theory to enable seamless internet collaboration.

By embracing CRDTs, OPFS SQLite, and WebRTC, we eliminate loading states, build inherently offline-proof software, and respect user privacy through E2EE. The cloud is no longer the center of our architecture—it is merely a convenient backup for the powerful supercomputers sitting in our users' pockets.

ArchitectureDistributed SystemsCRDTsLocal-FirstOffline