WebSocket Communication Patterns for Real-Time Updates in Plane

Plane implements real-time collaboration through a three-layer WebSocket architecture that combines Express-WS decorators, Yjs binary synchronization, and HTTP fallback mechanisms.

The open-source project management tool Plane enables live collaborative editing and board updates via a flexible WebSocket communication layer. This architecture bridges the Express-WS backend with Yjs documents on the frontend, ensuring low-latency updates while maintaining data consistency across unstable network conditions. The implementation follows a publish-subscribe model built on TypeScript decorators, binary message protocols, and automatic recovery systems.

Pattern 1: Decorator-Based WebSocket Controllers

Plane registers WebSocket endpoints using a custom @WebSocket method decorator that integrates with Express-WS. The decorator attaches metadata to controller methods, allowing the application to detect and register routes at startup.

In packages/decorators/src/websocket.ts, the decorator defines route metadata. When the server boots, registerWebSocketController (located in packages/decorators/src/controller.ts) scans controller prototypes for the "ws" metadata tag. For each decorated method, the registry creates an Express-WS route using router.ws('/api/v1/foo', handler).

The handler receives both the native ws object and the original HTTP request, enabling authentication header inspection before establishing the socket connection.

import { WebSocket } from "ws";
import { WebSocket as WSDecorator } from "@plane/decorators";

class BoardController {
  @WSDecorator("/boards/:id/updates")
  async boardUpdates(ws: WebSocket, req: Request) {
    // Authenticate the request
    const user = await authFromRequest(req);
    if (!user) return ws.close(1008, "Unauthenticated");

    // Join the Yjs document for the board
    const doc = await getYjsDoc(req.params.id);
    // Pipe Yjs updates to the socket
    doc.on("update", (update) => ws.send(update));
    ws.on("message", (msg) => doc.applyUpdate(msg as Uint8Array));
  }
}

Source: packages/decorators/src/controller.ts (lines 90-106)

Pattern 2: Yjs Binary Synchronization

The editor core implements collaborative state management through Yjs documents and a WebSocketProvider. In packages/editor/src/core/hooks/use-yjs-setup.ts, the application creates a Yjs document and connects it to the backend via the provider.

All document updates are encoded as binary messages and streamed through the WebSocket connection. The provider handles the Yjs protocol handshake (including version verification and document ID exchange) before streaming begins. This guarantees eventual consistency across all connected peers by forwarding binary updates to every client sharing the same document ID.

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

export function useYjsSetup(boardId: string) {
  const doc = new Y.Doc();
  const wsProvider = new WebsocketProvider(
    `${window.location.origin}/api/v1/boards/${boardId}/updates`,
    boardId,
    doc
  );

  wsProvider.on("status", (event) => {
    if (event === "disconnected") {
      console.warn("WebSocket lost – switching to HTTP fallback");
    }
  });

  return { doc, wsProvider };
}

Source: packages/editor/src/core/hooks/use-yjs-setup.ts (lines 108-124)

Pattern 3: HTTP Fallback for Connection Resilience

When WebSocket connections drop due to network instability, Plane implements graceful degradation to HTTP-based persistence. The usePageFallback hook in apps/web/core/hooks/use-page-fallback.ts monitors connection state and triggers backup save mechanisms.

The UI displays warnings when the socket disconnects, automatically switching to REST or GraphQL endpoints to persist changes. Upon reconnection, the WebSocketProvider re-synchronizes the document state, merging any changes saved via HTTP back into the Yjs document to resolve conflicts.

import { useEffect } from "react";

export function usePageFallback(isWsConnected: boolean) {
  useEffect(() => {
    if (!isWsConnected) {
      console.warn(
        "Websocket Connection lost, your changes are being saved using backup mechanism."
      );
    }
  }, [isWsConnected]);
}

Source: apps/web/core/hooks/use-page-fallback.ts (line 41)

How the Communication Flow Works

The WebSocket communication follows a structured lifecycle that ensures real-time synchronization with persistence guarantees:

  1. Controller Registration – At startup, registerController scans for @WebSocket metadata and binds routes to the Express-WS router using router.ws('/api/v1/foo', handler).

  2. Client Connection – The frontend initializes a WebSocketProvider targeting the registered endpoint URL. The provider performs a handshake exchanging protocol version and document identifiers.

  3. Message Streaming –

    • Client to Server: Local changes trigger Yjs binary updates that the provider transmits over the socket
    • Server to Clients: The WS handler receives binary blobs and rebroadcasts them to all sockets sharing the same document ID
    • Server Persistence: The handler may invoke service-layer methods to persist updates to the database
  4. Recovery Protocol – Connection failures trigger the fallback hook, switching to HTTP persistence while queuing changes. On reconnection, Yjs automatically reconciles the document state across all peers.

Summary

  • Decorator Registration: Plane uses @WebSocket decorators in packages/decorators/src/websocket.ts to register Express-WS routes dynamically at startup
  • Binary Synchronization: Yjs documents in packages/editor/src/core/hooks/use-yjs-setup.ts stream binary updates through WebSocketProvider for milliseconds-level latency
  • Graceful Degradation: The use-page-fallback.ts hook detects disconnections and switches to HTTP persistence, with automatic reconciliation on reconnect
  • Auth Integration: WebSocket handlers receive the original HTTP request object, enabling token-based authentication before socket establishment

Frequently Asked Questions

How does Plane authenticate WebSocket connections?

WebSocket handlers receive the original HTTP request object alongside the ws instance, as implemented in packages/decorators/src/controller.ts. The handler extracts authentication headers from the request object before establishing the socket connection, returning a 1008 close code if validation fails.

What happens when the WebSocket connection drops during editing?

The application detects disconnections through the wsProvider status events in packages/editor/src/core/hooks/use-yjs-setup.ts and triggers the usePageFallback hook from apps/web/core/hooks/use-page-fallback.ts. Changes persist via HTTP fallback mechanisms, and the UI displays warning messages until the connection restores.

Why does Plane use Yjs instead of JSON for real-time updates?

Plane leverages Yjs because it provides conflict-free replicated data types (CRDTs) that guarantee eventual consistency across clients. The binary encoding minimizes payload size compared to JSON, reducing bandwidth requirements for collaborative editing sessions with frequent updates.

How are WebSocket routes registered in the Plane backend?

Routes register automatically at startup when registerWebSocketController scans controller prototypes for the "ws" metadata injected by the @WebSocket decorator. This pattern, located in packages/decorators/src/controller.ts, creates Express-WS routes using router.ws() without requiring manual route configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →