Offline-First Synchronization: Syncing Loro CRDTs over WebRTC DataChannels
Master decentralized collaboration. Sync ultra-fast Rust-powered Loro CRDT document states directly peer-to-peer across browsers over WebRTC.

Master decentralized collaboration. Sync ultra-fast Rust-powered Loro CRDT document states directly peer-to-peer across browsers over WebRTC.
Offline-First Synchronization: Syncing Loro CRDTs over WebRTC DataChannels
Most collaborative applications depend on a central database server (like Google Docs or Figma) to receive client updates, order edits, and resolve collisions. While this is straightforward, it forces a dependency on expensive server infrastructure and breaks when users go offline.
Local-First architectures prioritize client-side data ownership. By using Conflict-Free Replicated Data Types (CRDTs), documents can merge conflict-free on any device in any order.
While Yjs is the standard JS CRDT library, Loro is the next-generation, high-performance CRDT framework written in Rust (compiled to WebAssembly). It is up to 100x faster than traditional JS CRDTs.
In this guide, we'll design a decentralized collaborative workspace that synchronizes Loro CRDT document states directly peer-to-peer (P2P) between browser tabs over WebRTC DataChannels.
⚡ 1. The P2P Synchronization Architecture
In a WebRTC P2P sync system, there is no master server. Devices connect directly via encrypted peer channels.
- 2.Loro Doc (Local): Holds the local document state. Any keypress triggers a local update.
- 4.Export Local Updates: When a local change occurs, Loro generates a lightweight binary delta (update packet).
- 6.WebRTC DataChannel: Delivers this binary packet directly to the connected peer over UDP.
- 8.Import Peer Updates: The receiving peer imports the binary delta into their local Loro Doc. Loro resolves logical clocks instantly and merges changes.
[User A Types] ──> [Loro Doc (A)] ──(Export Delta Binary)
│
[WebRTC DataChannel P2P Link]
│
(Import Delta Binary) ──> [Loro Doc (B)] ──> [User B Screen Updates]
🏗️ 2. Instantiating the Loro Document
Let's initialize our Loro document in JavaScript. We'll set up a shared map for document metadata and a shared text object for the main text editor.
javascriptimport { Loro } from 'loro-crdt'; class CollaborativeDocument { constructor() { // 1. Initialize Loro Document this.doc = new Loro(); // 2. Create a shared text buffer this.text = this.doc.getText('editor-buffer'); // 3. Setup change listener this.text.subscribe((event) => { if (event.local) { // Local edit: export change binary to send to peers const update = this.doc.export({ mode: "update" }); this.broadcastUpdateToPeers(update); } else { // Remote edit: update our UI this.updateTextareaUI(); } }); } // Receive dynamic remote updates from peer connections receivePeerUpdate(binaryUpdate) { // Import peer updates; Loro merges conflict-free! this.doc.import(binaryUpdate); this.updateTextareaUI(); } updateTextareaUI() { const textarea = document.getElementById('collab-textarea'); if (textarea && textarea.value !== this.text.toString()) { textarea.value = this.text.toString(); } } }
💻 3. Setting Up the WebRTC P2P DataChannel
To connect two browsers peer-to-peer, we use WebRTC. We'll write a clean wrapper to initialize an RTCPeerConnection and open a reliable binary RTCDataChannel.
javascriptclass PeerConnectionManager { constructor(signalingServerUrl, roomId, onBinaryReceived) { this.signaling = new WebSocket(signalingServerUrl); this.roomId = roomId; this.onBinaryReceived = onBinaryReceived; this.peerConn = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); this.setupSignaling(); this.setupDataChannel(); } setupDataChannel() { // Create data channel (configured for reliable transmission) this.dataChannel = this.peerConn.createDataChannel('loro-sync', { ordered: true }); this.dataChannel.binaryType = 'arraybuffer'; this.dataChannel.onmessage = (event) => { const arrayBuffer = event.data; const binaryData = new Uint8Array(arrayBuffer); // Callback to import updates into Loro this.onBinaryReceived(binaryData); }; // Handle incoming data channel from target peer this.peerConn.ondatachannel = (event) => { const channel = event.channel; channel.binaryType = 'arraybuffer'; channel.onmessage = (e) => { this.onBinaryReceived(new Uint8Array(e.data)); }; this.dataChannel = channel; }; } setupSignaling() { this.signaling.onmessage = async (message) => { const data = JSON.parse(message.data); if (data.offer) { await this.peerConn.setRemoteDescription(new RTCSessionDescription(data.offer)); const answer = await this.peerConn.createAnswer(); await this.peerConn.setLocalDescription(answer); this.sendSignal({ answer }); } else if (data.answer) { await this.peerConn.setRemoteDescription(new RTCSessionDescription(data.answer)); } else if (data.candidate) { await this.peerConn.addIceCandidate(new RTCIceCandidate(data.candidate)); } }; this.peerConn.onicecandidate = (event) => { if (event.candidate) { this.sendSignal({ candidate: event.candidate }); } }; } sendSignal(payload) { this.signaling.send(JSON.stringify({ roomId: this.roomId, ...payload })); } sendBinary(data) { if (this.dataChannel && this.dataChannel.readyState === 'open') { this.dataChannel.send(data.buffer); } } }
🚀 4. Connecting Loro and WebRTC together
Let's link the Loro document and our WebRTC data manager together to complete the real-time sync cycle.
javascriptconst collabDoc = new CollaborativeDocument(); const peerManager = new PeerConnectionManager( 'wss://signaling.sachinsharma.dev', 'collaborative-room-101', (binaryData) => { // 1. Peer sends update -> Import into local Loro Document collabDoc.receivePeerUpdate(binaryData); } ); // 2. Bind Loro updates to broadcast through the WebRTC data channel collabDoc.broadcastUpdateToPeers = (uint8ArrayUpdate) => { peerManager.sendBinary(uint8ArrayUpdate); }; // 3. Connect text inputs to local Loro updates const editorTextarea = document.getElementById('collab-textarea'); editorTextarea.addEventListener('input', (event) => { const value = event.target.value; // Calculate character diffs and update Loro const currentLength = collabDoc.text.toString().length; collabDoc.text.delete(0, currentLength); collabDoc.text.insert(0, value); });
📊 5. Synchronization Performance
By compiling the core CRDT mathematical engines down to WASM (using Loro) and bypassing server-side routing entirely:
- Latency: Reduced from ~80ms (client-to-server-to-client) to ~5ms (direct client-to-peer local network transit).
- Server Cost: Reduced to $0 (after signaling handshake, traffic flows entirely peer-to-peer).
- Security: Encrypted using WebRTC DTLS/SRTP protocols natively, preventing middle-man server inspection.
🏁 6. Conclusion
Building collaborative features on the web no longer requires complex server databases. By linking Rust-engineered WASM CRDT libraries like Loro with WebRTC P2P DataChannels, you build robust, offline-first collaborative systems that deliver sub-10ms performance, complete user data ownership, and zero server hosting fees.

Designing a Multi-Region Postgres Topology: Read Replicas, Logical Replication, and Safe Failover
A production-grade guide to designing highly available, low-latency multi-region PostgreSQL databases using logical replication, proxy geo-routing, and automated failover mechanics.

Building a Collaborative Whiteboard with WebRTC Mesh and Yjs CRDTs: Zero-Server Real-Time Vector Drawing
Learn how to build a fully decentralized real-time collaborative whiteboard. Synchronize dynamic freehand vectors and cursors using WebRTC and Yjs CRDTs.