Skip to main content
Technology & EngineeringRealtime Services232 lines

Yjs CRDT

Yjs is a high-performance, open-source CRDT implementation for building robust collaborative applications.

Quick Summary13 lines
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 lines
Paste into your CLAUDE.md or agent config

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.

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 a ydoc.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-websocket for server-backed, centralized collaboration. Opt for y-webrtc for 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.Doc or using Y.Map for 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.Map or Y.Array contents directly (e.g., `ymap.get('key

Install this skill directly: skilldb add realtime-services-skills

Get CLI access →