DEV Community

Cover image for Building Offline-First Collaborative Editors with CRDTs and IndexedDB (No Backend Needed)
HexShift
HexShift

Posted on

Building Offline-First Collaborative Editors with CRDTs and IndexedDB (No Backend Needed)

Modern collaborative tools like Notion, Google Docs, and Linear are powered by real-time sync and conflict resolution. But did you know you can build similar collaboration without any backend at all?

In this article, we’ll build an offline-first collaborative editor using Yjs (a CRDT library), storing changes locally via IndexedDB, and syncing them manually or peer-to-peer. No server, no database — just local-first magic.


Step 1: Install Yjs and the IndexedDB Adapter

Yjs is a powerful CRDT implementation that enables real-time syncing and automatic merge conflict resolution.

 npm install yjs y-indexeddb 

Then import them in your React app:

 import * as Y from 'yjs'; import { IndexeddbPersistence } from 'y-indexeddb'; 

Step 2: Create a Shared Yjs Document

You start by creating a shared Y.Doc, which will hold all collaborative state.

 const ydoc = new Y.Doc(); 

Hook up persistence to IndexedDB:

 const persistence = new IndexeddbPersistence('my-doc', ydoc); persistence.on('synced', () => { console.log('Loaded from IndexedDB'); }); 

This allows your doc to persist across page loads, offline sessions, and reboots — entirely on the client.


Step 3: Bind to a Shared Text Field

You can now define a shared text CRDT structure:

 const yText = ydoc.getText('editor'); 

Listen for changes:

 yText.observe(event => { console.log('Text updated:', yText.toString()); }); 

Step 4: Bind to a React Component

Let’s bind this collaborative CRDT to a React text editor (e.g., a ``):

 function CollaborativeEditor() { const [text, setText] = useState(''); useEffect(() => { const updateText = () => setText(yText.toString()); yText.observe(updateText); updateText(); return () => yText.unobserve(updateText); }, []); const handleChange = (e) => { yText.delete(0, yText.length); yText.insert(0, e.target.value); }; return ( <textarea value={text} onChange={handleChange} /> ); } 

Now all changes go through Yjs and are automatically conflict-resolved, even across tabs.


Step 5: Add Peer-to-Peer Sync with WebRTC (Optional)

You can optionally add peer-to-peer sync using the Yjs WebRTC provider:

 npm install y-webrtc 
 import { WebrtcProvider } from 'y-webrtc'; const provider = new WebrtcProvider('room-id', ydoc); 

Now all participants in the same "room" will share updates live — no backend required.


Step 6: Add Manual Sync Export/Import

Allow users to export/import their local doc for offline collaboration:

 function exportDoc() { const update = Y.encodeStateAsUpdate(ydoc); const blob = new Blob([update], { type: 'application/octet-stream' }); saveAs(blob, 'doc.ydoc'); } function importDoc(file) { const reader = new FileReader(); reader.onload = () => { const update = new Uint8Array(reader.result); Y.applyUpdate(ydoc, update); }; reader.readAsArrayBuffer(file); } 

This allows manual collaboration workflows (think encrypted file sharing or offline USB syncing).


Pros:

  • 📴 Works offline by default
  • 🔁 Real-time sync with CRDTs, no merge conflicts
  • 🧠 Full local persistence using IndexedDB
  • 🌐 Optional peer-to-peer sync with no server
  • 🧩 Useful for local Notion clones, knowledge bases, journaling apps

⚠️ Cons:

  • 🧪 No centralized backend = harder coordination
  • 🔄 Syncing large docs or media requires custom work
  • 🔐 Must build your own auth/encryption for secure sharing
  • 🧱 IndexedDB quirks across browsers

Summary

Using Yjs, IndexedDB, and optionally WebRTC, you can build fully offline-capable collaborative apps with real-time CRDT syncing — no backend required. This opens doors for local-first, privacy-respecting tools that work even with no internet. Whether you’re prototyping a Notion-like app or building custom knowledge tools, this technique lets you go far with very little infrastructure.


If this was helpful, you can support me here: Buy Me a Coffee

Top comments (0)