Distributed State Architecture

Real-Time Collaborative Apps in Node.js: WebSockets, CRDTs, and State Sync

Building multi-player collaboration into enterprise applications (canvases, rich-text documents, and spreadsheets) used to require complex centralized Operational Transformation (OT) servers. With Conflict-Free Replicated Data Types (CRDTs) like Yjs and Automerge paired with Node.js and Redis, teams can deliver zero-conflict, peer-to-peer, and offline-capable synchronization at massive scale.

Real-Time Collaborative Apps in Node.js WebSockets CRDT state sync architecture
Production Summary

CRDTs guarantee strong eventual consistency by mathematically structuring state mutations so that any two clients that receive the same set of updates—in any arbitrary order—will always converge to the exact same document state without central locking or server-side coordinate translation.

Core ParadigmConflict-Free Replicated Data Types (CRDTs)Tech StackNode.js, WebSockets, Yjs, Redis Pub/Sub, React 19Capacity100,000+ concurrent state merges / sec

CRDTs vs Operational Transformation (OT)

Early collaborative systems (like Google Docs) pioneered Operational Transformation. However, OT requires all edits to pass sequentially through a single authoritative central server to transform character index offsets. If a user goes offline or packets arrive out of order, OT complexity explodes exponentially.

FeatureOperational Transformation (OT)CRDTs (Yjs / Automerge)
Central Server RequirementMandatory authoritative sequencerOptional (works peer-to-peer or client-server)
Offline CapabilityExtremely hard; large rebase conflictsNative; merge weeks of offline work seamlessly
ComplexityCombinatorial transformation functions ($O(N^2)$)Mathematical convergence axioms ($O(1)$)
Network TransportsStrict FIFO delivery requiredIdempotent; works over WebSockets, WebRTC, BLE

Building the Horizontally Scalable Yjs WebSocket Server

When scaling across multiple Node.js Kubernetes pods, clients in the same document room might connect to different server instances. We bridge instances using Redis Pub/Sub channels:

// server.ts - Scalable Yjs WebSocket Server with Redis PubSub
import { WebSocketServer, WebSocket } from "ws";
import * as Y from "yjs";
import * as syncProtocol from "y-protocols/sync";
import * as awarenessProtocol from "y-protocols/awareness";
import Redis from "ioredis";

const pub = new Redis(process.env.REDIS_URL);
const sub = new Redis(process.env.REDIS_URL);
const docs = new Map<string, Y.Doc>();

function getOrCreateDoc(docName: string): Y.Doc {
  let doc = docs.get(docName);
  if (!doc) {
    doc = new Y.Doc();
    docs.set(docName, doc);

    // Listen to local document updates and publish to Redis cluster
    doc.on("update", (update: Uint8Array, origin: any) => {
      if (origin !== "redis") {
        const updateBase64 = Buffer.from(update).toString("base64");
        pub.publish(`yjs:${docName}`, updateBase64);
      }
    });

    // Subscribe to multi-instance updates
    sub.subscribe(`yjs:${docName}`);
  }
  return doc;
}

sub.on("message", (channel, message) => {
  const docName = channel.replace("yjs:", "");
  const doc = docs.get(docName);
  if (doc) {
    const update = Buffer.from(message, "base64");
    Y.applyUpdate(doc, update, "redis");
  }
});

export function setupWebSocketCollaboration(wss: WebSocketServer) {
  wss.on("connection", (conn: WebSocket, req) => {
    const docName = req.url?.slice(1) || "default-room";
    const doc = getOrCreateDoc(docName);

    // Step 1: Send initial SyncStep1 message to client
    const encoder = syncProtocol.createSyncStep1Message(doc);
    conn.send(encoder);

    // Step 2: Handle incoming binary sync messages
    conn.on("message", (message: ArrayBuffer) => {
      const uint8 = new Uint8Array(message);
      syncProtocol.readSyncMessage(uint8, doc, conn);
    });

    conn.on("close", () => {
      // Clean up memory if room is empty
      if (doc.isDestroyed) docs.delete(docName);
    });
  });
}

Whenever a client sends a delta update to Server A, it is applied to the local memory document and instantly fanned out via Redis to Server B and C, ensuring zero-latency multi-pod synchronization.

React 19 Client Integration & Live Cursor Awareness

Presence features (such as live multiplayer cursor coordinates, selection highlights, and user avatars) are ephemeral. The y-protocols/awareness module handles broadcast without dirtying persistent document state:

// useCollaborativeEditor.ts - React 19 Client Integration
import { useEffect, useState } from "react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

export function useCollaborativeEditor(roomId: string, user: { name: string; color: string }) {
  const [ydoc] = useState(() => new Y.Doc());
  const [provider, setProvider] = useState<WebsocketProvider | null>(null);
  const [awarenessUsers, setAwarenessUsers] = useState<any[]>([]);

  useEffect(() => {
    const wsProvider = new WebsocketProvider(
      process.env.NEXT_PUBLIC_WS_ENDPOINT || "wss://ws.endurancesoftwares.com",
      roomId,
      ydoc
    );

    // Broadcast user presence/cursor
    wsProvider.awareness.setLocalStateField("user", {
      name: user.name,
      color: user.color,
    });

    wsProvider.awareness.on("change", () => {
      const states = Array.from(wsProvider.awareness.getStates().values());
      setAwarenessUsers(states.map((s: any) => s.user).filter(Boolean));
    });

    setProvider(wsProvider);

    return () => {
      wsProvider.destroy();
      ydoc.destroy();
    };
  }, [roomId, ydoc, user.name, user.color]);

  return { ydoc, provider, awarenessUsers };
}

Document Persistence: Compact Snapshots in PostgreSQL & S3

CRDT updates are incremental binary blobs. Storing every keystroke in a relational database creates unbounded row bloat. Follow the Compaction Snapshot Pattern:

  • Write Buffer: Buffer raw Yjs update fragments in Redis for up to 10 seconds.
  • Squash & Encode: Periodically run Y.encodeStateAsUpdate(ydoc) to compact thousands of tiny edits into a single compressed binary state buffer.
  • PostgreSQL BLOB Storage: Store the merged state in a bytea column or upload to AWS S3 for long-term document history versioning.

Offline Resilience & Instant Reconnection

Using IndexedDB persistence adapters (like y-indexeddb), users can edit documents on airplanes or in unstable mobile networks. When connectivity resumes, the WebSocket provider automatically sends a compact vector clock diff, reconciling days of offline edits within milliseconds without prompt dialogs or loss of work.

Real-Time Architecture Checklist

✓ Binary WebSockets protocol used instead of JSON strings

✓ Redis Pub/Sub enables seamless multi-server cluster routing

✓ Ephemeral awareness layer separates cursors from document state

✓ IndexedDB client caching enables 100% offline document editing

✓ Periodic state compaction prevents CRDT memory bloat

✓ TLS / WSS encryption with JWT authorization tokens on handshake

✓ Graceful disconnection handling and document memory cleanup

✓ Vector clock conflict tests automated in test suite

Build Collaborative Web Apps with Endurance Softwares

Our senior full-stack engineers build real-time multi-user canvases, diagramming tools, and interactive SaaS portals powered by WebSockets and CRDTs.

Discuss Your Real-Time Project
Shares

Request Free Consultation

Frequently Asked Questions

How does Yjs prevent memory leaks on long-running documents?

Yjs uses a highly optimized struct store. Calling Y.encodeStateAsUpdate() purges deleted metadata and compacts item tombstones, keeping active memory under a few megabytes even for book-length documents.

Can CRDTs handle rich text with formatting attributes?

Yes! Y.Text supports rich-text formatting (bold, italics, links, custom attributes) natively and integrates seamlessly with modern rich text editors like ProseMirror, TipTap, Quill, and Lexical.

What happens if two users edit the exact same word simultaneously?

CRDTs deterministically interleave the characters based on their unique client IDs and clock sequence numbers. No characters are ever lost, and both screens arrive at identical strings.

Get Quote
Let's build something powerful

Have a project idea? Let’s turn it into a scalable product.

Book Free Consultation

© 2026 Endurance Softwares. All rights reserved.