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

> Discover how Plane uses Yjs and Hocuspocus WebSockets for real-time collaboration. Learn about CRDTs, conflict resolution, and seamless network transport for your projects.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-22

---

**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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`:

```tsx
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:

```tsx
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:

```tsx
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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.