Yjs CRDT
Yjs is a high-performance, open-source CRDT implementation for building robust collaborative applications.
You are a CRDT expert specializing in Yjs, a powerful library for building real-time, collaborative web applications. You design systems where multiple users can concurrently edit shared content, from rich text documents to complex data structures, knowing that conflicts are automatically resolved and data integrity is maintained. You architect for resilience, ensuring applications function seamlessly offline and synchronize efficiently when connectivity is restored, integrating Yjs with various network providers like WebSockets and WebRTC. ## Key Points * **Directly modifying Yjs object properties:** ## Quick Example ```bash npm install yjs y-websocket ```
skilldb get realtime-services-skills/Yjs CRDTFull skill: 232 linesYou are a CRDT expert specializing in Yjs, a powerful library for building real-time, collaborative web applications. You design systems where multiple users can concurrently edit shared content, from rich text documents to complex data structures, knowing that conflicts are automatically resolved and data integrity is maintained. You architect for resilience, ensuring applications function seamlessly offline and synchronize efficiently when connectivity is restored, integrating Yjs with various network providers like WebSockets and WebRTC.
Core Philosophy
Yjs embraces the philosophy of Conflict-free Replicated Data Types (CRDTs), which are data structures that can be replicated across multiple machines, updated independently and concurrently, and then merged without requiring complex conflict resolution logic. This inherent property makes Yjs an ideal choice for collaborative applications where a central server might not always be the single source of truth, or where offline capabilities are paramount. Changes made by any client can be applied in any order, and the resulting state will always converge.
The core strength of Yjs lies in its "local-first" approach. Every client maintains a complete, local copy of the shared document and applies changes instantly. When network connectivity is available, these local changes are broadcast and merged with other clients' updates. This provides an incredibly responsive user experience, as latency is minimized. Yjs is also entirely provider-agnostic, meaning it handles the CRDT logic, while you choose the underlying transport layer (e.g., WebSockets, WebRTC, custom HTTP long-polling) that best fits your application's architecture.
You choose Yjs when your application demands robust real-time collaboration, especially for complex data types like rich text, arrays, or nested objects, and requires strong guarantees around data consistency and offline usability. It excels in scenarios like shared whiteboards, collaborative code editors, or synchronized data dashboards where multiple users need to interact with the same application state simultaneously and reliably.
Setup
Integrate Yjs into your project by installing the core library and a suitable network provider. The most common provider for server-backed collaboration is y-websocket.
npm install yjs y-websocket
Then, initialize a Y.Doc and connect it to a WebsocketProvider.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// 1. Create a Y.Doc instance. This is the shared document.
const ydoc = new Y.Doc();
// 2. Connect to a WebSocket server using y-websocket.
// The first argument is the WebSocket server URL.
// The second argument is the room name (e.g., 'my-document-id').
// The third argument is the Y.Doc instance to synchronize.
const provider = new WebsocketProvider(
'ws://localhost:1234', // Replace with your WebSocket server URL
'my-collaborative-room', // A unique identifier for this document
ydoc
);
// Optional: Listen for connection status changes
provider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {
console.log(`WebSocket status: ${event.status}`);
});
// You now have a Y.Doc that will synchronize with other clients
// connected to the same room via the WebSocket server.
On the server side, you'll need a y-websocket compatible server, often implemented using Node.js and ws.
// server.js (example using y-websocket's default server)
import { WebSocketServer } from 'ws';
import { setupWSConnection } from 'y-websocket/bin/utils'; // Use this for simplicity
const wss = new WebSocketServer({ port: 1234 });
wss.on('connection', (ws, req) => {
console.log('Client connected');
// Initialize Y.js connection for each client
setupWSConnection(ws, req, {
gc: true // Garbage collect old data (default: true)
});
});
console.log('Yjs WebSocket server listening on ws://localhost:1234');
Key Techniques
1. Shared Text Editing with Y.Text
For collaborative text editors, Y.Text is the fundamental type. It handles character-level insertions, deletions, and formatting, automatically merging concurrent changes without conflicts.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('ws://localhost:1234', 'text-editor-room', ydoc);
// Get a shared text type from the document
const ytext = ydoc.getText('my-shared-text');
// Bind to a simple <textarea> for demonstration
const textarea = document.createElement('textarea');
textarea.style.width = '100%';
textarea.style.height = '200px';
document.body.appendChild(textarea);
// 1. Observe changes from other clients and update the textarea
ytext.observe(event => {
// Update the textarea's value without moving the cursor if possible
if (textarea.value !== ytext.toString()) {
const currentCursorPos = textarea.selectionStart;
textarea.value = ytext.toString();
// Try to restore cursor position
textarea.setSelectionRange(currentCursorPos, currentCursorPos);
}
});
// 2. Update Y.Text when the textarea changes
textarea.addEventListener('input', () => {
// Yjs transactions batch multiple changes into a single update.
ydoc.transact(() => {
// Clear the current Y.Text content and insert the new content.
// More efficient binding libraries exist (e.g., y-textarea, y-codemirror).
// This simple approach works but might lose cursor position.
ytext.delete(0, ytext.length);
ytext.insert(0, textarea.value);
});
});
// Initialize textarea with current Y.Text content
textarea.value = ytext.toString();
// Example: Programmatically insert text
ydoc.transact(() => {
ytext.insert(0, 'Hello, ');
ytext.insert(ytext.length, 'world!');
});
2. Shared Data Structures with Y.Map and Y.Array
Yjs provides Y.Map for key-value storage and Y.Array for ordered lists, both of which are CRDTs and automatically synchronize changes.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('ws://localhost:1234', 'data-room', ydoc);
// Get a shared Map and Array type
const ymap = ydoc.getMap('my-shared-map');
const yarray = ydoc.getArray('my-shared-array');
// Observe changes on Y.Map
ymap.observe(event => {
console.log('Map changed:', event.changes.keys);
console.log('Current Map state:', ymap.toJSON());
});
// Observe changes on Y.Array
yarray.observe(event => {
console.log('Array changed:', event.delta); // delta describes changes
console.log('Current Array state:', yarray.toJSON());
});
// Perform updates within a transaction
ydoc.transact(() => {
// Y.Map operations
ymap.set('title', 'Collaborative Document');
ymap.set('version', 1);
ymap.set('tags', ['yjs', 'crdt', 'realtime']);
// Y.Array operations
yarray.push(['Item 1']); // Push a new item
yarray.insert(0, ['New First Item']); // Insert at specific index
yarray.delete(1, 1); // Delete 1 item at index 1 (Item 1)
yarray.push([new Y.Map()]); // Yjs types can be nested!
const nestedMap = yarray.get(yarray.length - 1) as Y.Map<any>;
nestedMap.set('id', 'nested-item');
});
// Accessing data
console.log('Initial title:', ymap.get('title'));
console.log('Array length:', yarray.length);
3. Offline Persistence with IndexedDB
Yjs documents can persist their state locally using providers like y-indexeddb, enabling offline-first experiences. When the application reloads or internet connectivity returns, the Y.Doc can load its last known state and then synchronize any new changes.
import * => Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { IndexeddbPersistence } from 'y-indexeddb';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('ws://localhost:1234', 'persistent-room', ydoc);
// 1. Create an IndexeddbPersistence instance
// It will automatically load the document state from IndexedDB
// and save changes to IndexedDB.
const persistence = new IndexeddbPersistence('my-document-name', ydoc);
persistence.on('synced', () => {
console.log('Initial content loaded from IndexedDB');
// Now the Y.Doc is ready to be used, potentially with offline content.
// Any changes made while offline will be saved locally
// and synced when the WebsocketProvider connects.
const ytext = ydoc.getText('shared-content');
console.log('Current content:', ytext.toString());
// Example: Add content. This will be saved to IndexedDB and synced.
if (ytext.length === 0) {
ydoc.transact(() => {
ytext.insert(0, 'This is a persistent document. ');
});
}
});
// You can explicitly check if IndexedDB is synced, though 'synced' event is often sufficient.
// persistence.whenSynced.then(() => {
// console.log('IndexedDB is fully synced with Y.Doc');
// });
Best Practices
- Batch updates with
ydoc.transact(): Wrap multiple Yjs operations within aydoc.transact(() => { ... })block. This ensures all changes are applied atomically and broadcast as a single update, reducing network overhead and preventing intermediate inconsistent states for observers. - Decouple Yjs from UI: Avoid directly manipulating DOM elements or complex UI state within
y.observe()callbacks. Instead, update a reactive state management system (e.g., React state, Vuex, MobX) which then renders the UI, ensuring a clean separation of concerns. - Choose the Right Provider: Use
y-websocketfor server-backed, centralized collaboration. Opt fory-webrtcfor peer-to-peer, serverless collaboration where clients connect directly. Consider a custom provider for unique transport needs. - Handle Provider Connection Status: Monitor the
provider.on('status', ...)event to provide user feedback about synchronization status (e.g., "Connecting...", "Online", "Offline"). This improves user experience and helps debug issues. - Use Unique Document Names: Ensure each distinct collaborative document has a unique room name or document ID when initializing your provider (
WebsocketProvider('...', 'unique-room-id', ydoc)). This prevents accidental mixing of data. - Store Metadata Separately (Often): While Yjs can store any data, for application-specific metadata that doesn't need CRDT properties (e.g., user preferences, transient UI state), consider storing it outside the
Y.Docor usingY.Mapfor simple key-value pairs. - Implement Authentication/Authorization at the Provider Level: Yjs itself does not handle user authentication or fine-grained authorization. Implement these security layers on your WebSocket server or WebRTC signaling server to control who can access and modify documents.
Anti-Patterns
- Directly modifying Yjs object properties:
Direct Mutation. Attempting to modify
Y.MaporY.Arraycontents directly (e.g., `ymap.get('key
Install this skill directly: skilldb add realtime-services-skills
Related Skills
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels.
Centrifugo
Centrifugo is a high-performance, real-time messaging server that handles WebSocket,
Convex Realtime
Integrate Convex for a real-time backend with reactive queries, transactional mutations, and automatic
Electric SQL
Integrate ElectricSQL to build local-first, real-time applications with a PostgreSQL backend.
Firebase Realtime Db
Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,
Liveblocks
Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage,