The Complete Guide to Building Real-Time Collaborative Apps with Yjs in 2026
Yjs became the dominant CRDT (Conflict-free Replicated Data Type) library for building collaborative applications in 2025-2026. Google Docs, Notion, and Figma all use similar technology. Yjs makes it possible to build real-time collaborative features without a PhD in distributed systems.
Here's the practical guide.
What is a CRDT
Operational Transformation (OT): Complex, server-dependent
CRDT: Simple, peer-to-peer capable, eventual consistency guaranteed
Yjs uses a specific CRDT algorithm (YATA) that handles concurrent edits correctly without a central server.
Basic Setup
npm install yjs y-websocket
Y.Doc with Text
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
const doc = new Y.Doc();
// Get the shared text type
const ytext = doc.getText("my-text");
// Update from remote
ytext.observe((event) => {
console.log("Content changed:", ytext.toString());
});
// Make local changes
ytext.insert(0, "Hello"); // Insert at position 0
ytext.delete(0, 1); // Delete 1 char at position 0
// Get current content
console.log(ytext.toString());
WebSocket Sync
// Server (Node.js)
import { WebSocketServer } from "ws";
import { setupWSConnection } from "y-websocket/bin/utils";
const wss = new WebSocketServer({ port: 1234 });
wss.on("connection", (ws, req) => {
setupWSConnection(ws, req);
});
// Client
const doc = new Y.Doc();
const provider = new WebsocketProvider(
"ws://localhost:1234",
"my-room",
doc
);
provider.on("status", (event) => {
console.log("Connection status:", event.status);
});
provider.on("sync", (isSynced) => {
console.log("Synced:", isSynced);
});
Y.Map for Structured Data
const ymap = doc.getMap("user-preferences");
// Set values
ymap.set("theme", "dark");
ymap.set("notifications", true);
// Get values
console.log(ymap.get("theme")); // "dark"
// Observe changes
ymap.observe((event) => {
event.keysChanged.forEach((key) => {
console.log(`${key} changed to:`, ymap.get(key));
});
});
Y.Array for Lists
const yarray = doc.getArray("tasks");
// Add items
yarray.push([{ id: 1, text: "Task 1", done: false }]);
// Insert at position
yarray.insert(0, [{ id: 0, text: "First task", done: false }]);
// Remove
yarray.delete(0, 1);
// Observe
yarray.observe((event) => {
event.changes.delta.forEach((d) => {
if (d.insert) console.log("Inserted:", d.insert);
if (d.delete) console.log("Deleted:", d.delete.length, "items");
});
});
With Monaco Editor
import * as monaco from "monaco-editor";
import { MonacoBinding } from "y-monaco";
const doc = new Y.Doc();
const provider = new WebsocketProvider("ws://localhost:1234", "editor", doc);
const ytext = doc.getText("monaco");
const editor = monaco.editor.create(document.getElementById("editor"), {
value: "",
});
const binding = new MonacoBinding(
ytext,
editor.getModel(),
new Set([editor]),
provider.awareness
);
Awareness (Cursors)
// Set your presence
provider.awareness.setLocalStateField("user", {
name: "Alice",
color: "#ff6b6b",
});
// Listen to others
provider.awareness.on("change", () => {
const states = provider.awareness.getStates();
states.forEach((state, clientId) => {
if (clientId !== doc.clientID && state.user) {
console.log(`${state.user.name} is here`);
}
});
});
This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.
Ready to Build Your Online Business?
Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.
Top comments (0)