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:
-
Controller Registration – At startup,
registerControllerscans for@WebSocketmetadata and binds routes to the Express-WS router usingrouter.ws('/api/v1/foo', handler). -
Client Connection – The frontend initializes a
WebSocketProvidertargeting the registered endpoint URL. The provider performs a handshake exchanging protocol version and document identifiers. -
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
-
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
@WebSocketdecorators inpackages/decorators/src/websocket.tsto register Express-WS routes dynamically at startup - Binary Synchronization: Yjs documents in
packages/editor/src/core/hooks/use-yjs-setup.tsstream binary updates through WebSocketProvider for milliseconds-level latency - Graceful Degradation: The
use-page-fallback.tshook 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →