Building Offline Collaborative Apps with CRDTs
Local-First Applications and Join-Semilattices
The paradigm of modern software design is shifting rapidly toward local-first architectures. Users expect applications to be instantly responsive, fully functional offline, and capable of seamless real-time collaboration when online.
Historically, building collaborative systems required complex centralized synchronization protocols like Operational Transformation (OT)—the technology behind Google Docs. However, OT is notoriously difficult to implement, requiring a central authority to order and transform every edit.
Conflict-Free Replicated Data Types (CRDTs) offer a mathematically sound alternative. By embedding merge rules directly within the data structures, CRDTs enable multi-user collaboration across peer-to-peer and offline environments without relying on a single source of truth.
To understand the power of CRDTs, it is necessary to examine their mathematical foundations. A CRDT is a distributed data structure designed to be replicated across multiple nodes in a network, with two primary types: State-based CRDTs (CvRDTs) and Operation-based CRDTs (CmRDTs).
CvRDTs synchronize by exchanging their entire state (or state deltas). When a node receives a remote state, it merges it with its local state using a custom merge function. For a CvRDT to guarantee consistency, this merge function must form a bounded join-semilattice.
Mathematically, this means the merge operation must satisfy three core algebraic properties: commutativity, associativity, and idempotency. These properties ensure that regardless of network latency, packet loss, or out-of-order delivery, all nodes that receive the same set of updates will eventually converge on the exact same state.
State-Based versus Operation-Based Replication
When selecting a CRDT architecture, engineers must choose between two primary replication types, each presenting distinct trade-offs:
- State-based CvRDTs: Replicas synchronize by merging entire states. They are easier to implement and highly resilient to network packet duplication and loss, but they incur higher network costs as the state grows large.
- Operation-based CmRDTs: Replicas transmit only the discrete change operations (e.g., "insert character at index 5"). They require lower bandwidth but depend on a reliable transport layer that guarantees causal, in-order delivery of operations.
- Delta-state CvRDTs: An optimized hybrid model where nodes track and send only the state changes (deltas) accumulated since the last synchronization event, combining the reliability of CvRDTs with the performance of CmRDTs.
Implementing a Last-Write-Wins Register
Let’s implement a basic Last-Write-Wins Register (LWW-Register) in JavaScript. This is a common register CRDT where the state consists of a value and a hybrid logical timestamp.
If two clients edit the register simultaneously, the update with the higher timestamp wins. Because clock drift makes physical clocks unreliable, CRDTs use wall-clock timestamps paired with client identifiers or logical counters to ensure uniqueness and total ordering.
class LWWRegister {
constructor(clientId, value = null, timestamp = 0) {
this.clientId = clientId;
this.value = value;
this.timestamp = timestamp;
}
// Update the register with a new value
set(newValue) {
this.value = newValue;
this.timestamp = Date.now();
return this.getState();
}
// Get serializable state representation
getState() {
return {
value: this.value,
timestamp: this.timestamp,
clientId: this.clientId
};
}
// Merge local state with a remote state
merge(remoteState) {
if (remoteState.timestamp > this.timestamp) {
this.value = remoteState.value;
this.timestamp = remoteState.timestamp;
} else if (remoteState.timestamp === this.timestamp) {
// Tie-breaker: lexicographically sort client IDs
if (remoteState.clientId > this.clientId) {
this.value = remoteState.value;
this.timestamp = remoteState.timestamp;
}
}
}
}
Client-Side Persistence and State Vector Synchronization
While registers are simple, real-world applications require more complex data structures like collaborative text documents, arrays, and nested maps. Popular libraries like Yjs and Automerge abstract these complexities by representing documents as structured trees of operations.
In these systems, every character inserted in a text document is assigned a unique identifier (typically a client ID and a sequential logical clock value called a Lamport timestamp). Deleted characters are not removed immediately; instead, they are marked with a "tombstone" to ensure that concurrent operations can still locate their relative insertion points.
Although tombstones can bloat memory over time, modern engines use advanced run-length encoding (RLE) to compress operations, making sync payloads extremely small and efficient.
Network Transport Topologies and Signaling
Architecting the storage and transport layers for local-first apps requires careful engineering. On the client side, state must be persisted locally in IndexedDB or SQLite (via WASM) so the application remains functional when the browser is closed or offline.
When the device reconnects, it initiates a synchronization handshake. To minimize network payload sizes, clients exchange state vectors—compact arrays representing the range of operations they have already processed.
The server or peer then uses this state vector to calculate the exact diff (or delta) of missing operations and streams it back. This delta-based synchronization makes the network communication incredibly fast, even for documents containing thousands of historical edits.
CRDT Synchronization at the Edge with Bramsley
To enable real-time collaboration, a transport layer must route these deltas between clients. WebSockets are commonly used for client-server topologies, while WebRTC allows direct peer-to-peer syncing. However, a pure peer-to-peer setup struggles with scale and firewalls.
A hybrid approach uses edge-computed WebSockets and lightweight edge runtimes as signaling servers and persistent coordinators. By hosting CRDT-aware synchronization points at the network edge, developers can drastically reduce coordination latency and ensure that offline users reconnect to a local node that is physically close to them, accelerating merge times and ensuring high-concurrency consistency.