# How Plane Implements Real-Time Collaboration with Hocuspocus and Yjs

> Discover how Plane uses Hocuspocus and Yjs for real-time collaboration. Learn about CRDT documents, WebSocket providers, and offline caching for seamless editing.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-23

---

**Plane leverages the `useYjsSetup` React hook to instantiate a Hocuspocus WebSocket provider, bind it to a Yjs CRDT document, and manage the full connection lifecycle—including automatic reconnection, forced-close handling, and IndexedDB persistence for offline caching.**

The open-source Plane project (makeplane/plane) enables real-time collaborative document editing by integrating Hocuspocus, a WebSocket-based provider, with Yjs, a conflict-free replicated data type (CRDT) library. This architecture ensures multiple users can simultaneously edit documents without conflicts, maintaining state consistency even across network interruptions or browser tab sleeps.

## Architecture Overview

Plane's collaboration system consists of four key components working together:

- **`useYjsSetup` hook** – Located 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)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/hooks/use-yjs-setup.ts), this hook instantiates the Hocuspocus provider, maintains the Yjs document lifecycle, and handles connection states, reconnections, and IndexedDB persistence.

- **Collaboration Context** – The [[`collaboration-context.tsx`](https://github.com/makeplane/plane/blob/main/collaboration-context.tsx)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/contexts/collaboration-context.tsx) file wraps the hook's output in a React context, making the provider and Yjs document available throughout the component tree.

- **Yjs Utilities** – Helper functions in [[`yjs-utils.ts`](https://github.com/makeplane/plane/blob/main/yjs-utils.ts)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/helpers/yjs-utils.ts) handle fragment operations and deep observation of Yjs data structures.

- **Editor Integration** – The Tiptap/ProseMirror editor receives the `ydoc` through a Yjs plugin, enabling shared mutable state across connected clients.

## The `useYjsSetup` Hook Implementation

The core of Plane's real-time collaboration lives in the `useYjsSetup` hook. This section breaks down how it initializes providers, manages connection states, and ensures data persistence.

### Provider Initialization and Authentication

When the hook mounts, it creates a new `HocuspocusProvider` instance configured with the document ID, authentication token, and server URL. According to lines 62-66 of [[`use-yjs-setup.ts`](https://github.com/makeplane/plane/blob/main/use-yjs-setup.ts)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/hooks/use-yjs-setup.ts), the initialization looks like this:

```typescript
const provider = new HocuspocusProvider({
  name: docId,
  token: authToken,
  url: serverUrl,
  // Event callbacks defined below
});

```

This establishes the WebSocket connection to the Hocuspocus server and wires it to a `Y.Doc` instance that serves as the single source of truth for document state.

### Connection Lifecycle and State Management

The hook translates WebSocket events into a typed `CollabStage` state that the UI can consume. The state machine includes stages like `connecting`, `awaiting-sync`, `synced`, `reconnecting`, and `disconnected`.

The implementation defines specific handlers for connection events in lines 73-100:

- **`onConnect`** – Triggered when the WebSocket opens
- **`onStatus`** – Monitors WebSocket status changes  
- **`onSynced`** – Fires when the client state synchronizes with the server

These handlers update the React state to reflect the current collaboration stage, allowing UI components to display connection status in real time.

### Handling Forced Closures and Reconnections

Plane distinguishes between manual disconnects and server-initiated forced closures. The hook monitors close codes in the range 4000-4003 as forced closes. When detected, the `handleClose` logic (lines 141-191) pauses the provider and implements retry logic with backoff.

For automatic recovery, the hook listens to browser visibility and online events:

- **`handleVisibilityChange`** – Detects when the user returns to the tab after it was backgrounded
- **`handleOnline`** – Responds to browser online/offline events

These handlers, located in lines 198-250 of [[`use-yjs-setup.ts`](https://github.com/makeplane/plane/blob/main/use-yjs-setup.ts)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/hooks/use-yjs-setup.ts), trigger reconnection attempts to ensure the editor recovers after network glitches or system sleeps.

### Offline Persistence with IndexedDB

To support offline editing, Plane uses `IndexeddbPersistence` to mirror the Yjs document locally. Lines 70-84 of the source file show the effect that creates this persistence layer:

```typescript
const persistence = new IndexeddbPersistence(docId, ydoc);
persistence.on('synced', () => {
  setHasCachedContent(true);
  setIsCacheReady(true);
});

```

Once the local cache is ready, the hook sets `hasCachedContent` and `isCacheReady` to true, allowing the UI to render cached data immediately—even before the server connection establishes.

## Integrating with the Tiptap Editor

With the `useYjsSetup` hook providing the Yjs document, integrating collaborative editing into the Tiptap editor requires passing the document fragment to the Yjs extension.

```tsx
import { useYjsSetup } from '@/hooks/use-yjs-setup';
import { EditorContent, useEditor } from '@tiptap/react';
import { yjsPlugin } from '@tiptap/extension-yjs';

function CollaborativeEditor() {
  // Initialize Yjs collaboration
  const { provider, ydoc, state } = useYjsSetup({
    docId: 'my-project-123',
    serverUrl: process.env.NEXT_PUBLIC_HOCUSPOCUS_URL!,
    authToken: userToken,
    onStateChange: (s) => console.log('Collab state →', s.kind),
  });

  // Create editor with Yjs integration
  const editor = useEditor({
    extensions: [
      // Other extensions...
      yjsPlugin(ydoc.getXmlFragment('default')),
    ],
    content: '',
  });

  return (
    <>
      <EditorContent editor={editor} />
      {state.stage.kind === 'connecting' && <span>Connecting…</span>}
      {state.stage.kind === 'disconnected' && <span>Offline – retrying…</span>}
    </>
  );
}

```

The `yjsPlugin` from `@tiptap/extension-yjs` receives `ydoc.getXmlFragment('default')`, which binds the ProseMirror editor state to the shared Yjs document. This enables real-time synchronization of text changes across all connected clients.

## React Context Propagation

To avoid prop drilling, Plane wraps the `useYjsSetup` output in the **Collaboration Context** defined in [[`collaboration-context.tsx`](https://github.com/makeplane/plane/blob/main/collaboration-context.tsx)](https://github.com/makeplane/plane/blob/preview/packages/editor/src/core/contexts/collaboration-context.tsx). This context provides the provider, Yjs document, connection state, and action methods to any descendant component.

When the connection state changes, the hook invokes an optional `onStateChange` callback (lines 131-144 in the source), allowing UI components like connection status banners to react instantly to stage transitions.

## Summary

- **HocuspocusProvider** manages the WebSocket connection to the collaboration server, handling authentication and message broadcasting.
- **`useYjsSetup`** encapsulates provider initialization, connection lifecycle management, forced-close handling, and automatic reconnection logic.
- **Yjs** provides the conflict-free CRDT data layer through `Y.Doc`, ensuring consistent document state across clients.
- **IndexedDB persistence** caches the document locally, enabling offline editing and immediate content availability.
- The **Collaboration Context** and React hooks make the infrastructure accessible throughout the editor component tree.
- **Tiptap integration** occurs through the Yjs plugin, which binds the editor's ProseMirror state to the shared Yjs document fragment.

## Frequently Asked Questions

### What is Hocuspocus and why does Plane use it for real-time collaboration?

**Hocuspocus** is a WebSocket-based provider built specifically for Yjs. Plane uses it because it handles the complexities of WebSocket connections, reconnection logic, and message broadcasting automatically. According to the Plane source code, Hocuspocus manages the transport layer while Yjs handles the data consistency, creating a robust foundation for collaborative editing.

### How does Plane handle network interruptions during collaborative editing?

Plane implements automatic reconnection through the `useYjsSetup` hook's `handleVisibilityChange` and `handleOnline` handlers. These functions detect when the browser regains connectivity or when the user returns to the tab after it was backgrounded. The hook also distinguishes forced closes (close codes 4000-4003) from normal disconnections, allowing it to implement specific retry logic for server-initiated closures.

### What role does IndexedDB play in Plane's collaboration system?

IndexedDB provides offline persistence for the Yjs document. The `useYjsSetup` hook creates an `IndexeddbPersistence` instance that mirrors the document state locally. This allows users to see cached content immediately upon opening the editor, even before the WebSocket connection establishes, and enables editing to continue during offline periods with synchronization occurring once connectivity returns.

### How is the Yjs document shared across React components in Plane?

Plane uses a React Context defined in [`collaboration-context.tsx`](https://github.com/makeplane/plane/blob/main/collaboration-context.tsx) to distribute the Yjs document, Hocuspocus provider, and connection state throughout the component tree. The `useYjsSetup` hook generates these values at the root level, and the context provider makes them available to child components without prop drilling, ensuring consistent access to the collaborative editing infrastructure.