How Plane Implements Real-Time Collaborative Document Editing Using Tiptap and Hocuspocus
Plane implements real-time collaborative document editing by combining Tiptap's ProseMirror-based editor with Yjs CRDTs and Hocuspocus WebSocket providers, enabling conflict-free concurrent editing across clients with automatic reconnection and server-side persistence.
The open-source project management platform makeplane/plane delivers seamless collaborative editing through a tightly integrated stack of three core technologies. This architecture allows multiple users to simultaneously edit page content and titles without locking mechanisms, while maintaining data consistency through conflict-free replicated data types (CRDTs) and providing resilience against network interruptions.
Architecture Overview
Plane's collaborative editing stack rests on three pillars that handle distinct responsibilities:
- Tiptap provides the ProseMirror-based rich-text editing interface on the client.
- Yjs acts as the CRDT engine that holds the document model and merges concurrent changes.
- Hocuspocus supplies both the client-side WebSocket provider (
@hocuspocus/provider) and the server-side WebSocket router (@hocuspocus/server) that synchronizes Yjs updates between clients and persists them to storage.
This separation ensures that the user interface remains decoupled from synchronization logic, while the server focuses solely on routing updates and managing persistence extensions.
Client-Side Initialization
Setting Up the HocuspocusProvider
The entry point for real-time collaboration begins in the use-yjs-setup.ts hook located at packages/editor/src/core/hooks/use-yjs-setup.ts. This hook initializes the WebSocket connection and manages the document lifecycle.
At lines 62-66, the hook creates a HocuspocusProvider instance with the document ID, authentication token, and server URL:
// packages/editor/src/core/hooks/use-yjs-setup.ts (lines 62-66)
const provider = new HocuspocusProvider({
url: serverUrl,
name: docId,
token: authToken,
// ... additional configuration
});
The provider installs callbacks for connection lifecycle events including onConnect, onStatus, onSynced, and onClose. When the client receives the synced event (lines 100-104), the hook updates its internal state to reflect that the document is ready for editing.
To support offline-first behavior, the hook also initializes IndexeddbPersistence at lines 77-81 via the onIdbSynced callback. This caches the Yjs document locally in the browser's IndexedDB, allowing immediate rendering of cached content while the network synchronization proceeds in the background.
Configuring Tiptap with the Collaboration Extension
Once the provider is established, the use-collaborative-editor.ts hook (located at packages/editor/src/core/hooks/use-collaborative-editor.ts) wires Tiptap to the shared Yjs document.
The hook constructs the editor's extension array, inserting the Collaboration extension twice: once for the main document body and once for the page title. At lines 80-84, the configuration binds the main content to the default field of the Yjs document:
// packages/editor/src/core/hooks/use-collaborative-editor.ts (lines 80-84)
Collaboration.configure({
document: provider.document,
field: "default", // Main content XmlFragment
}),
For the title field, lines 66-70 configure a separate collaboration instance:
// packages/editor/src/core/hooks/use-collaborative-editor.ts (lines 66-70)
Collaboration.configure({
document: provider.document,
field: "title", // Title XmlFragment
}),
This dual-field approach allows concurrent editing of both the document title and body as separate entities within the same shared Y.Doc.
Server-Side Infrastructure
The Hocuspocus Server Singleton
On the server side, Plane uses a singleton pattern to manage the Hocuspocus instance. The apps/live/src/hocuspocus.ts file declares the HocusPocusServerManager class, which initializes the WebSocket server at lines 45-53:
// apps/live/src/hocuspocus.ts (lines 45-53)
const hocuspocus = new Hocuspocus({
port: parseInt(process.env.PORT || "3000"),
address: process.env.HOST || "0.0.0.0",
onAuthenticate,
onStateless,
...getExtensions(),
});
This singleton ensures that all WebSocket connections route through a single server instance, optimizing memory usage and maintaining consistent state across the application.
Authentication and Persistence Extensions
Security and durability are handled through Hocuspocus extensions. At lines 47-48, the server registers an onAuthenticate callback that validates the JWT token provided by the client during the initial handshake.
For persistence, the server loads extensions at lines 49-50 that include @hocuspocus/extension-redis for pub/sub capabilities and @hocuspocus/extension-database for long-term storage of the Yjs document state. These extensions ensure that document updates survive server restarts and remain available to late-joining collaborators.
The Synchronization Flow
When a user types in the Tiptap editor, the following chain executes:
- Local Mutation: Tiptap's
Collaborationextension captures the ProseMirror transaction and translates it into Yjs operations on the sharedY.Doc. - CRDT Merge: Yjs automatically merges these operations with any concurrent changes from other users, eliminating conflicts without requiring locks.
- WebSocket Transmission: The
HocuspocusProviderforwards the Yjs update message through the open WebSocket connection to the server. - Server Broadcast: The Hocuspocus server receives the update and broadcasts it to all other connected clients subscribed to the same document ID.
- Remote Application: Other clients receive the update, apply it to their local
Y.Docinstances, and Tiptap's ProseMirror view re-renders only the changed nodes, creating a near-real-time collaborative experience.
Resilience and Offline-First Capabilities
Plane's implementation handles network instability through sophisticated reconnection logic in use-yjs-setup.ts. The hook monitors onClose events at lines 41-70 to distinguish between forced disconnections (error codes 4000-4003, typically authentication failures) and transient network losses.
For transient failures, the hook implements automatic reconnection with exponential backoff up to a configurable DEFAULT_MAX_RETRIES limit. It also listens for page visibility and online/offline browser events (lines 71-92) to intelligently pause synchronization when the user switches tabs or loses connectivity, then resume seamlessly when the network returns.
The IndexedDB persistence layer ensures that users can continue editing during complete offline periods, with changes syncing automatically once the connection restores.
Summary
- Three-tier architecture: Tiptap handles the UI, Yjs manages the CRDT document model, and Hocuspocus provides WebSocket transport and server-side persistence.
- Dual-field collaboration: Plane separates the document title and body into distinct Yjs fields (
"title"and"default") within a single shared document, configured inuse-collaborative-editor.ts. - Offline-first design: IndexedDB caching in
use-yjs-setup.tsallows immediate content rendering and continued editing during network interruptions. - Automatic resilience: The client implements intelligent reconnection logic that distinguishes fatal errors from transient network issues, with configurable retry limits.
- Server extensibility: The Hocuspocus server in
apps/live/src/hocuspocus.tssupports authentication hooks and persistence extensions for Redis and database storage.
Frequently Asked Questions
How does Plane handle simultaneous edits from multiple users?
Plane uses Yjs CRDTs (Conflict-free Replicated Data Types) to merge concurrent edits automatically. When multiple users type simultaneously, Yjs applies their respective operations to the shared document without requiring locks or causing version conflicts. Tiptap's Collaboration extension translates these merged changes into ProseMirror transactions, ensuring all clients converge to the same document state regardless of network latency.
What happens when a user loses internet connection while editing?
The client remains functional through the IndexedDB persistence layer initialized in use-yjs-setup.ts. Local edits continue to apply to the cached Yjs document stored in the browser. When connectivity returns, the HocuspocusProvider automatically synchronizes the pending changes with the server. The hook also monitors browser online/offline events to pause sync attempts during disconnection and resume them upon reconnection.
How does the server persist collaborative documents?
The Hocuspocus server in apps/live/src/hocuspocus.ts loads persistence extensions including @hocuspocus/extension-database and @hocuspocus/extension-redis. These extensions store the authoritative Yjs document state to the database and Redis cache respectively, ensuring that document changes survive server restarts and remain available for users who join the collaborative session later.
Can the collaborative editing features work across different document types?
Yes, the architecture is document-agnostic. The use-yjs-setup.ts hook accepts any docId parameter to initialize the provider, and the use-collaborative-editor.ts hook configures the Tiptap editor with the specific extensions needed for that document type. As long as the server recognizes the document ID and the client has the appropriate Tiptap extensions loaded, the same Yjs and Hocuspocus infrastructure supports any collaborative editing scenario within Plane.
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 →