Building Real-Time Collaborative Cursors with Yjs and WebSockets

Introduction to Real-Time Presence

In modern SaaS applications, multi-user collaboration is a baseline expectation. Showing where other users are active in real-time provides essential context and eliminates collaboration collision.

However, tracking and rendering dozens of real-time cursors per document introduces significant engineering challenges. The system must process high-frequency mouse coordinates, sync them with sub-100ms latency, and handle connection drops gracefully without causing layout shifting.

Traditional client-server polling is too slow and resource-intensive. Instead, modern collaborative platforms rely on persistent WebSocket connections combined with Conflict-Free Replicated Data Types (CRDTs). By distributing the synchronization logic and delegating user presence to a specialized awareness protocol, developers achieve smooth, conflict-free rendering.

Understanding Yjs and CRDTs

Yjs is a high-performance CRDT library specifically optimized for collaborative applications. Unlike Operational Transformation (OT), which requires a centralized server to serialize and resolve operations, CRDTs allow replica nodes to update their local state independently and merge conflicts mathematically without a coordinator.

For cursor tracking, we do not need persistent document state; instead, we use Yjs’s Awareness Protocol. The awareness protocol is an out-of-band communication channel built alongside Yjs that propagates transient state, such as selection ranges, usernames, cursor colors, and active coordinates. This state is stored in-memory, syncs rapidly via WebSockets using state vectors, and is automatically garbage-collected when a user disconnects.

Designing the Cursor Awareness State Architecture

To build a scalable cursor synchronization system, the application must manage three key stages:

  • Coordinate Normalization: Raw pixel values vary across screen resolutions. To ensure cursors appear in the correct logical location, client coordinates must be normalized to relative percentages (X and Y coordinates between 0 and 1) based on the bounding box of the shared workspace.
  • State Dissemination: The client updates its local awareness state at a throttled interval (e.g., every 16ms or 33ms) to avoid saturating the network. The WebSocket proxy relays these state packets to all peer connections.
  • Garbage Collection and Cleanup: If a client closes their tab or loses internet connection, the server must detect the socket closure and broadcast an offline state event so other clients can remove the disconnected user's cursor element from the DOM.

Technical Implementation: Yjs and WebSocket Awareness Client

The following implementation demonstrates how to configure a Yjs document, bind a WebSocket provider, capture throttled cursor movements, scale coordinates relatively, and update the collaborative canvas dynamically:

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

export class CollaborativeWorkspace {
  constructor(roomId, localUser, canvasElement) {
    this.canvas = canvasElement;
    this.doc = new Y.Doc();
    
    // Initialize the WebSocket provider connecting to the edge gateway
    this.provider = new WebsocketProvider(
      'wss://api.bramsley.network/collaboration',
      roomId,
      this.doc
    );
    
    this.awareness = this.provider.awareness;

    // Set local client details
    this.awareness.setLocalState({
      user: {
        name: localUser.name,
        color: localUser.color
      },
      cursor: null // Coordinates stored as { x: fraction, y: fraction }
    });

    this.setupPresenceListeners();
  }

  setupPresenceListeners() {
    // Capture and throttle local cursor movements relative to the workspace container
    let throttleTimeout = null;
    this.canvas.addEventListener('mousemove', (event) => {
      if (throttleTimeout) return;

      throttleTimeout = setTimeout(() => {
        throttleTimeout = null;
        const rect = this.canvas.getBoundingClientRect();
        const x = (event.clientX - rect.left) / rect.width;
        const y = (event.clientY - rect.top) / rect.height;

        // Update local awareness state; propagates automatically via WebSockets
        this.awareness.setLocalStateField('cursor', { x, y });
      }, 33); // Sync at ~30Hz to optimize bandwidth
    });

    // Handle mouse leaving the workspace area
    this.canvas.addEventListener('mouseleave', () => {
      this.awareness.setLocalStateField('cursor', null);
    });

    // Listen to changes in peer awareness states
    this.awareness.on('change', () => {
      const states = this.awareness.getStates();
      this.renderRemoteCursors(states);
    });
  }

  renderRemoteCursors(states) {
    states.forEach((state, clientID) => {
      // Do not render local user's cursor
      if (clientID === this.doc.clientID) return;

      let cursorNode = document.getElementById(`cursor-${clientID}`);

      if (state.cursor && state.user) {
        if (!cursorNode) {
          cursorNode = document.createElement('div');
          cursorNode.id = `cursor-${clientID}`;
          cursorNode.className = 'absolute pointer-events-none transition-all duration-75 ease-out';
          
          // Render visual cursor elements
          const pointer = document.createElement('div');
          pointer.className = 'w-4 h-4 border-2 border-white rounded-full shadow-md';
          pointer.style.backgroundColor = state.user.color;

          const label = document.createElement('span');
          label.className = 'ml-4 px-1.5 py-0.5 rounded text-white text-xs font-semibold';
          label.style.backgroundColor = state.user.color;
          label.innerText = state.user.name;

          cursorNode.appendChild(pointer);
          cursorNode.appendChild(label);
          this.canvas.appendChild(cursorNode);
        }

        // Project relative percentages to actual pixels on the canvas
        const rect = this.canvas.getBoundingClientRect();
        cursorNode.style.left = `${state.cursor.x * rect.width}px`;
        cursorNode.style.top = `${state.cursor.y * rect.height}px`;
      } else if (cursorNode) {
        // Remove cursor element if peer disconnected or cursor went null
        cursorNode.remove();
      }
    });
  }

  destroy() {
    this.provider.destroy();
    this.doc.destroy();
  }
}

Scaling Collaborative Real-Time Awareness at the Edge with Bramsley

Running WebSocket servers at a single central location introduces high latency for international users, turning smooth multiplayer cursors into frustrating, lagging dots.

How Bramsley Powers Collaborative Presence

By shifting WebSocket gateway logic and room orchestration directly to global edge workers, Bramsley guarantees low-latency, conflict-free state synchronization:

  • Edge-Native Room Routing: Automatically spin up localized presence rooms at the closest physical node using Cloudflare Durable Objects.
  • Intelligent Protocol Fallbacks: Mitigate connection disruptions by dynamically falling back to WebTransport or Server-Sent Events.
  • Throttled Message Broadcasts: Optimize client bandwidth by aggregating and batching cursor coordinate telemetry directly on edge worker runtimes.

Partner with Bramsley to build instant, zero-lag multiplayer experiences for your enterprise SaaS applications. Connect with our engineering team today to deploy edge-optimized collaboration systems.

Bramsley Digital Studio

Enterprise Digital Architecture

We engineer digital infrastructure that drives measurable B2B growth. Experts in Legacy System Migration and High-Performance Frontends.

Architecture Specs & Case Studies

Scale Your Operations

  • Legacy System Migration
  • Scalable Infrastructure
  • High-Performance Frontends
  • Global Edge Deployment