How Plane Implements Real-Time Collaboration Using Yjs and Hocuspocus WebSockets

Plane achieves real-time collaborative editing by combining Yjs CRDTs for automatic conflict resolution with Hocuspocus WebSocket providers for network transport, wrapping both in React hooks that manage connection state, IndexedDB caching, and intelligent reconnection logic.

Plane's open-source project management platform (makeplane/plane) enables seamless multi-user document editing through a robust collaboration architecture. The implementation centers on the useYjsSetup hook in packages/editor/src/core/hooks/use-yjs-setup.ts and the CollaborationProvider context, which together orchestrate WebSocket connections, offline persistence, and graceful handling of network interruptions.

WebSocket Connection Architecture with Hocuspocus

Initializing the HocuspocusProvider

The collaboration layer instantiates a HocuspocusProvider within the useYjsSetup hook, establishing a WebSocket connection to Plane's server using the document ID, authentication token, and server URL.

According to the source code in packages/editor/src/core/hooks/use-yjs-setup.ts, this provider automatically handles the WebSocket handshake, exchanges binary Yjs updates, and validates the authentication token on every connection attempt. The hook returns the provider instance alongside the Yjs document, enabling downstream editor components to interact with the live synchronization layer.

Connection Event Handling

The provider tracks critical lifecycle events including onConnect, onStatus, onSynced, onAuthenticationFailed, and close. These events drive a state machine that exposes a CollaborationState to React components, reporting precise stages such as connecting, awaiting-sync, synced, reconnecting, and disconnected.

Yjs Document and CRDT Structure

Shared Document Initialization

For each collaborative session, Plane creates a Y.Doc instance (accessible via provider.document). This document serves as the conflict-free replicated data type (CRDT) that maintains shared state across all connected clients without server-side locking.

Within the document, Plane stores editor content in an XML fragment named "default" (accessed via ydoc.getXmlFragment("default")). Editor plugins in packages/editor/src/core/plugins/ bind this fragment to ProseMirror or TipTap instances, enabling real-time cursor tracking and content merging.

Connection Lifecycle and Resilience

State Machine Implementation

The useYjsSetup hook implements a finite state machine that transitions through collaboration stages based on provider events. When connections drop, the system attempts reconnection with a capped retry limit of DEFAULT_MAX_RETRIES = 3, preventing infinite loops while maintaining availability during temporary network blips.

Handling Forced Disclosures

Plane distinguishes accidental disconnects from server-initiated terminations using forced-close detection. Custom WebSocket close codes ranging from 4000 to 4003, or an explicit signalForcedClose flag, trigger a "forced close" state. In this scenario, the provider pauses rather than immediately retrying, allowing the application to handle authentication failures or permission revocations gracefully.

Automatic Reconnection Logic

To recover from transient failures, the hook listens for browser events including visibilitychange, focus, and online. When the browser regains connectivity or visibility, the system re-enables wsProvider.shouldConnect and invokes connect(). A throttle mechanism prevents these reconnection attempts from firing twice in rapid succession, protecting against connection storms.

Local Persistence Strategy

IndexedDB Caching with y-indexeddb

Plane optimizes perceived performance through IndexeddbPersistence, which mirrors the Yjs document into the browser's IndexedDB via the y-indexeddb package. This local storage layer enables offline-first capabilities and instant content rendering.

When the local database synchronizes, the hook sets isCacheReady to true and reports whether cached content exists via hasCachedContent. This allows the UI to render immediately from local storage while the WebSocket connection finishes synchronizing remote changes in the background.

React Context Integration

CollaborationProvider Context

The CollaborationProvider component in packages/editor/src/core/contexts/collaboration-context.tsx calls useYjsSetup and supplies the returned object via a React context named CollabContext. This architecture centralizes collaboration logic and makes it accessible throughout the component tree without prop drilling.

Components consume this state through the useCollaboration() hook, which returns the provider, ydoc, current state, and action helpers like signalForcedClose.

External State Propagation

Whenever the internal collaboration stage changes, a useEffect hook within use-yjs-setup.ts invokes the optional onStateChange callback. This mechanism allows external UI layers to react to connection shifts—displaying loading spinners during initial sync, error banners on authentication failure, or offline indicators during disconnections.

Implementation Examples

Wrapping the Editor with CollaborationProvider

To enable real-time editing in a React application, wrap the editor component with CollaborationProvider:

import { CollaborationProvider } from "@/core/contexts/collaboration-context";
import { Editor } from "@/components/editor";

function Page({ docId }: { docId: string }) {
  return (
    <CollaborationProvider
      docId={docId}
      serverUrl="wss://collab.plane.so/hocuspocus"
      authToken={localStorage.getItem("planeToken")!}
      fallback={<div>Connecting…</div>}
    >
      <Editor />
    </CollaborationProvider>
  );
}

The provider initializes the useYjsSetup hook, establishes the WebSocket connection, and makes the Yjs document available to child components through React context.

Consuming Collaboration State

Components access the collaborative document and connection status using the useCollaboration hook:

import { useCollaboration } from "@/core/contexts/collaboration-context";

function Editor() {
  const {
    ydoc,
    provider,
    state: { isDocReady, stage },
    actions: { signalForcedClose },
  } = useCollaboration();

  // Disable UI while document syncs
  if (!isDocReady) return <Spinner />;

  // Access the shared XML fragment for ProseMirror/TipTap binding
  const yXmlFragment = ydoc.getXmlFragment("default");
  
  // Handle manual disconnect on logout
  const handleLogout = () => {
    signalForcedClose(true);
    provider.disconnect();
  };

  return (
    <div>
      {/* Editor implementation */}
    </div>
  );
}

Displaying Connection Status

The discriminated union stage object enables type-safe UI rendering based on connection health:

import { useCollaboration } from "@/core/contexts/collaboration-context";

function ConnectionStatus() {
  const { state } = useCollaboration();

  switch (state.stage.kind) {
    case "connecting":
      return <p>Connecting…</p>;
    case "awaiting-sync":
      return <p>Synchronising…</p>;
    case "synced":
      return <p>All changes synced.</p>;
    case "reconnecting":
      return <p>Reconnecting (attempt {state.stage.attempt})…</p>;
    case "disconnected":
      return <p>Disconnected: {state.stage.error?.message}</p>;
    default:
      return null;
  }
}

Summary

  • Plane's real-time collaboration relies on Yjs CRDTs managed through HocuspocusProvider WebSocket connections defined in packages/editor/src/core/hooks/use-yjs-setup.ts.
  • The CollaborationProvider context exposes document state, connection status, and lifecycle actions via the useCollaboration() hook.
  • Connection resilience features include capped retry logic (DEFAULT_MAX_RETRIES = 3), forced-close detection (codes 4000-4003), and automatic reconnection on browser visibility changes.
  • IndexeddbPersistence enables offline-first editing by caching documents locally while the WebSocket synchronizes in the background.
  • The XML fragment "default" within the Y.Doc stores the collaborative content, binding to ProseMirror or TipTap editor instances for conflict-free synchronization.

Frequently Asked Questions

What is the role of Hocuspocus in Plane's real-time collaboration system?

Hocuspocus acts as a WebSocket-aware wrapper around Yjs that handles connection management, authentication, and binary protocol transmission. In Plane's implementation, the HocuspocusProvider (instantiated in use-yjs-setup.ts) manages the WebSocket lifecycle, automatically exchanging Yjs updates between clients and validating JWT tokens on every connection attempt.

How does Plane handle network interruptions during collaborative editing?

Plane implements a resilient state machine that tracks connection stages including connecting, reconnecting, and disconnected. The system caps reconnection attempts at DEFAULT_MAX_RETRIES = 3 and listens for browser events like visibilitychange and online to trigger automatic reconnection. Forced disconnections (close codes 4000-4003) pause the provider rather than retrying, preventing infinite loops during server-initiated terminations.

What is the purpose of the "default" XML fragment in Plane's Yjs implementation?

The "default" XML fragment is the standard namespace within the Y.Doc where Plane stores collaborative editor content. Accessed via ydoc.getXmlFragment("default"), this fragment holds the rich text structure that synchronizes across all users. Editor plugins in packages/editor/src/core/plugins/ bind this fragment to ProseMirror or TipTap instances, enabling real-time cursor tracking and content merging.

How does Plane optimize initial document load times for collaborative editors?

Plane leverages IndexeddbPersistence from the y-indexeddb package to mirror the Yjs document in the browser's IndexedDB. When a user opens a document, the hook checks hasCachedContent and sets isCacheReady immediately, allowing the UI to render cached content while the WebSocket connection synchronizes remote changes in the background. This offline-first approach eliminates loading delays during network latency.

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 →