Features
- Multi-Provider Support: Enables real-time collaboration using Yjs and slate-yjs. Supports multiple synchronization providers simultaneously (e.g., IndexedDB + Hocuspocus + WebRTC) working on a shared
Y.Doc. - Built-in Providers: Includes support for IndexedDB local persistence, Hocuspocus server-based collaboration, and WebRTC peer-to-peer collaboration.
- Custom Providers: Extensible architecture allows adding custom providers by implementing the
UnifiedProviderinterface. - Awareness & Cursors: Integrates Yjs Awareness protocol for sharing cursor locations and other ephemeral state between users. Includes
RemoteCursorOverlayfor rendering remote cursors. - Customizable Cursors: Cursor appearance (name, color) can be customized via
cursors. - Manual Lifecycle: Provides explicit
initanddestroymethods for managing the Yjs connection lifecycle.
Usage
Installation
Install the core Yjs plugin.
pnpm add @platejs/yjspnpm add @platejs/yjsFor Hocuspocus server-based collaboration:
pnpm add @hocuspocus/providerpnpm add @hocuspocus/providerFor WebRTC peer-to-peer collaboration:
pnpm add y-webrtcpnpm add y-webrtcAdd Plugin
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin,
],
// Important: Skip Plate's default initialization when using Yjs
skipInitialization: true,
});import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin,
],
// Important: Skip Plate's default initialization when using Yjs
skipInitialization: true,
});It's crucial to set skipInitialization: true when creating the editor. Yjs manages the initial document state, so Plate's default value initialization should be skipped to avoid conflicts.
Configure YjsPlugin
Configure the plugin with providers and cursor settings:
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
import { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin.configure({
render: {
afterEditable: RemoteCursorOverlay,
},
options: {
// Configure local user cursor appearance
cursors: {
data: {
name: 'User Name', // Replace with dynamic user name
color: '#aabbcc', // Replace with dynamic user color
},
},
// Configure providers. All providers share the same Y.Doc and Awareness instance.
providers: [
// Example: IndexedDB provider for local persistence
{
type: 'indexeddb',
options: {
docName: 'my-document-id', // Unique IndexedDB database name
},
},
// Example: Hocuspocus provider
{
type: 'hocuspocus',
options: {
name: 'my-document-id', // Unique identifier for the document
url: 'ws://localhost:8888', // Your Hocuspocus server URL
},
},
// Example: WebRTC provider (can be used alongside Hocuspocus)
{
type: 'webrtc',
options: {
roomName: 'my-document-id', // Must match the document identifier
signaling: ['ws://localhost:4444'], // Optional: Your signaling server URLs
},
},
],
},
}),
],
skipInitialization: true,
});import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
import { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin.configure({
render: {
afterEditable: RemoteCursorOverlay,
},
options: {
// Configure local user cursor appearance
cursors: {
data: {
name: 'User Name', // Replace with dynamic user name
color: '#aabbcc', // Replace with dynamic user color
},
},
// Configure providers. All providers share the same Y.Doc and Awareness instance.
providers: [
// Example: IndexedDB provider for local persistence
{
type: 'indexeddb',
options: {
docName: 'my-document-id', // Unique IndexedDB database name
},
},
// Example: Hocuspocus provider
{
type: 'hocuspocus',
options: {
name: 'my-document-id', // Unique identifier for the document
url: 'ws://localhost:8888', // Your Hocuspocus server URL
},
},
// Example: WebRTC provider (can be used alongside Hocuspocus)
{
type: 'webrtc',
options: {
roomName: 'my-document-id', // Must match the document identifier
signaling: ['ws://localhost:4444'], // Optional: Your signaling server URLs
},
},
],
},
}),
],
skipInitialization: true,
});render.afterEditable: AssignsRemoteCursorOverlayto render remote user cursors.cursors.data: Configures the local user's cursor appearance with name and color.providers: Array of collaboration providers to use (Hocuspocus, WebRTC, or custom providers).
Add Editor Container
The RemoteCursorOverlay requires a positioned container around the editor content. Use EditorContainer component or PlateContainer from platejs/react:
import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);Initialize Yjs Connection
Yjs connection and state initialization are handled manually, typically within a useEffect hook:
import React, { useEffect } from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { useMounted } from '@/hooks/use-mounted'; // Or your own mounted check
const MyEditorComponent = ({ documentId, initialValue }) => {
const editor = usePlateEditor(/** editor config from previous steps **/);
const mounted = useMounted();
useEffect(() => {
// Ensure component is mounted and editor is ready
if (!mounted) return;
// Initialize Yjs connection, sync document, and set initial editor state
editor.getApi(YjsPlugin).yjs.init({
id: documentId, // Unique identifier for the Yjs document
value: initialValue, // Initial content if the Y.Doc is empty
});
// Clean up: Destroy connection when component unmounts
return () => {
editor.getApi(YjsPlugin).yjs.destroy();
};
}, [editor, mounted]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
};import React, { useEffect } from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { useMounted } from '@/hooks/use-mounted'; // Or your own mounted check
const MyEditorComponent = ({ documentId, initialValue }) => {
const editor = usePlateEditor(/** editor config from previous steps **/);
const mounted = useMounted();
useEffect(() => {
// Ensure component is mounted and editor is ready
if (!mounted) return;
// Initialize Yjs connection, sync document, and set initial editor state
editor.getApi(YjsPlugin).yjs.init({
id: documentId, // Unique identifier for the Yjs document
value: initialValue, // Initial content if the Y.Doc is empty
});
// Clean up: Destroy connection when component unmounts
return () => {
editor.getApi(YjsPlugin).yjs.destroy();
};
}, [editor, mounted]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
};Initial Value: The value passed to init is only used to populate the Y.Doc if it's completely empty on the backend/peer network. If the document already exists, its content will be synced, and this initial value will be ignored.
Lifecycle Management: You must call editor.api.yjs.init() to establish the connection and editor.api.yjs.destroy() on component unmount to clean up resources.
Monitor Connection Status (Optional)
Access provider states and add event handlers for connection monitoring:
import React from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { usePluginOption } from 'platejs/react';
function EditorStatus() {
// Access provider states directly (read-only)
const providers = usePluginOption(YjsPlugin, '_providers');
const isConnected = usePluginOption(YjsPlugin, '_isConnected');
return (
<div>
{providers.map((provider) => (
<span key={provider.type}>
{provider.type}: {provider.isConnected ? 'Connected' : 'Disconnected'} ({provider.isSynced ? 'Synced' : 'Syncing'})
</span>
))}
</div>
);
}
// Add event handlers for connection events:
YjsPlugin.configure({
options: {
// ... other options
onConnect: ({ type }) => console.debug(`Provider ${type} connected!`),
onDisconnect: ({ type }) => console.debug(`Provider ${type} disconnected.`),
onSyncChange: ({ type, isSynced }) => console.debug(`Provider ${type} sync status: ${isSynced}`),
onError: ({ type, error }) => console.error(`Error in provider ${type}:`, error),
},
});import React from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { usePluginOption } from 'platejs/react';
function EditorStatus() {
// Access provider states directly (read-only)
const providers = usePluginOption(YjsPlugin, '_providers');
const isConnected = usePluginOption(YjsPlugin, '_isConnected');
return (
<div>
{providers.map((provider) => (
<span key={provider.type}>
{provider.type}: {provider.isConnected ? 'Connected' : 'Disconnected'} ({provider.isSynced ? 'Synced' : 'Syncing'})
</span>
))}
</div>
);
}
// Add event handlers for connection events:
YjsPlugin.configure({
options: {
// ... other options
onConnect: ({ type }) => console.debug(`Provider ${type} connected!`),
onDisconnect: ({ type }) => console.debug(`Provider ${type} disconnected.`),
onSyncChange: ({ type, isSynced }) => console.debug(`Provider ${type} sync status: ${isSynced}`),
onError: ({ type, error }) => console.error(`Error in provider ${type}:`, error),
},
});Provider Types
Hocuspocus Provider
Server-based collaboration using Hocuspocus. Requires a running Hocuspocus server.
type HocuspocusProviderConfig = {
type: 'hocuspocus',
options: {
name: string; // Document identifier
url: string; // WebSocket server URL
token?: string; // Authentication token
wsOptions?: HocuspocusProviderWebsocketConfiguration; // Advanced websocket config (headers, protocols, etc.)
}
}type HocuspocusProviderConfig = {
type: 'hocuspocus',
options: {
name: string; // Document identifier
url: string; // WebSocket server URL
token?: string; // Authentication token
wsOptions?: HocuspocusProviderWebsocketConfiguration; // Advanced websocket config (headers, protocols, etc.)
}
}wsOptions
You can pass a wsOptions field to configure advanced websocket options for the Hocuspocus provider. This is useful for custom headers, authentication, protocols, or other websocket settings supported by HocuspocusProviderWebsocket.
Example usage:
{
type: 'hocuspocus',
options: {
name: 'my-document-id',
},
wsOptions: {
url: 'ws://localhost:8888',
maxAttempts: 5,
parameters: {
// request parameters
}
},
}{
type: 'hocuspocus',
options: {
name: 'my-document-id',
},
wsOptions: {
url: 'ws://localhost:8888',
maxAttempts: 5,
parameters: {
// request parameters
}
},
}WebRTC Provider
Peer-to-peer collaboration using y-webrtc.
type WebRTCProviderConfig = {
type: 'webrtc',
options: {
roomName: string; // Room name for collaboration
signaling?: string[]; // Signaling server URLs
password?: string; // Room password
maxConns?: number; // Max connections
peerOpts?: object; // WebRTC peer options
}
}type WebRTCProviderConfig = {
type: 'webrtc',
options: {
roomName: string; // Room name for collaboration
signaling?: string[]; // Signaling server URLs
password?: string; // Room password
maxConns?: number; // Max connections
peerOpts?: object; // WebRTC peer options
}
}IndexedDB Provider
Local browser persistence using y-indexeddb. Use it alongside a network provider when the editor should restore local state before remote sync finishes.
type IndexeddbProviderConfig = {
type: 'indexeddb',
options: {
docName: string; // Stable IndexedDB database name for this document
}
}type IndexeddbProviderConfig = {
type: 'indexeddb',
options: {
docName: string; // Stable IndexedDB database name for this document
}
}Custom Provider
Create custom providers by implementing the UnifiedProvider interface:
interface UnifiedProvider {
awareness: Awareness;
document: Y.Doc;
type: string;
connect: () => void;
destroy: () => void;
disconnect: () => void;
isConnected: boolean;
isSynced: boolean;
}interface UnifiedProvider {
awareness: Awareness;
document: Y.Doc;
type: string;
connect: () => void;
destroy: () => void;
disconnect: () => void;
isConnected: boolean;
isSynced: boolean;
}Use custom providers directly in the providers array:
const customProvider = new MyCustomProvider({ doc: ydoc, awareness });
YjsPlugin.configure({
options: {
providers: [customProvider],
},
});const customProvider = new MyCustomProvider({ doc: ydoc, awareness });
YjsPlugin.configure({
options: {
providers: [customProvider],
},
});Backend Setup
IndexedDB Local Persistence
IndexedDB runs in the browser and uses the shared Y.Doc created by YjsPlugin. Use the same document identifier for docName and your network provider room/name when you combine providers:
{
type: 'indexeddb',
options: {
docName: 'document-1',
},
}{
type: 'indexeddb',
options: {
docName: 'document-1',
},
}IndexedDB does not transport remote awareness or cursors. It persists document updates locally; Hocuspocus or WebRTC still own multi-user transport.
Hocuspocus Server
Set up a Hocuspocus server for server-based collaboration. Ensure the url and name in your provider options match your server configuration.
WebRTC Setup
Signaling Server
WebRTC requires signaling servers for peer discovery. Public servers work for testing but use your own for production:
pnpm add y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.jspnpm add y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.jsConfigure your client to use custom signaling:
{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
},
}{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
},
}TURN Servers
WebRTC connections can fail due to firewalls. Use TURN servers or combine with Hocuspocus for production reliability.
Configure TURN servers for reliable connections:
{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
peerOpts: {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'username',
credential: 'password'
}
]
}
}
}
}{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
peerOpts: {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'username',
credential: 'password'
}
]
}
}
}
}Security
Authentication & Authorization:
- Use Hocuspocus's
onAuthenticatehook to validate users - Implement document-level access control on your backend
- Pass authentication tokens via the
tokenoption
Transport Security:
- Use
wss://URLs in production for encrypted communication - Configure secure TURN servers with the
turns://protocol
WebRTC Security:
- Use the
passwordoption for basic room access control - Configure secure signaling servers
Example secure configuration:
YjsPlugin.configure({
options: {
providers: [
{
type: 'hocuspocus',
options: {
name: 'secure-document-id',
url: 'wss://your-hocuspocus-server.com',
token: 'user-auth-token',
},
},
{
type: 'webrtc',
options: {
roomName: 'secure-document-id',
password: 'strong-room-password',
signaling: ['wss://your-secure-signaling.com'],
peerOpts: {
config: {
iceServers: [
{
urls: 'turns:your-turn-server.com:443?transport=tcp',
username: 'user',
credential: 'pass'
}
]
}
}
},
},
],
},
});YjsPlugin.configure({
options: {
providers: [
{
type: 'hocuspocus',
options: {
name: 'secure-document-id',
url: 'wss://your-hocuspocus-server.com',
token: 'user-auth-token',
},
},
{
type: 'webrtc',
options: {
roomName: 'secure-document-id',
password: 'strong-room-password',
signaling: ['wss://your-secure-signaling.com'],
peerOpts: {
config: {
iceServers: [
{
urls: 'turns:your-turn-server.com:443?transport=tcp',
username: 'user',
credential: 'pass'
}
]
}
}
},
},
],
},
});Troubleshooting
Connection Issues
Check URLs and Names:
- Verify
url(Hocuspocus) andsignalingURLs (WebRTC) are correct - Ensure
docName,name, orroomNamematches exactly across providers that share one document - Use
ws://for local development,wss://for production
Server Status:
- Verify Hocuspocus and signaling servers are running
- Check server logs for errors
- Test TURN server connectivity if using WebRTC
Network Issues:
- Firewalls may block WebSocket or WebRTC traffic
- Use TURN servers configured for TCP (port 443) for better traversal
- Check browser console for provider errors
Multiple Documents
Separate Instances:
- Create separate
Y.Docinstances for each document - Use unique document identifiers for
name/roomName - Pass unique
ydocandawarenessinstances to each editor
Sync Issues
Editor Initialization:
- Always set
skipInitialization: truewhen creating the editor - Use
editor.api.yjs.init({ value })for initial content - Ensure all providers use the exact same document identifier
Content Conflicts:
- Avoid manually manipulating the shared
Y.Doc - Let Yjs handle all document operations through the editor
Cursor Issues
Overlay Setup:
- Include
RemoteCursorOverlayin plugin render config - Use positioned container (
EditorContainerorPlateContainer) - Verify
cursors.data(name, color) is set correctly for local user
Related
- Yjs - CRDT framework for collaboration
- slate-yjs - Yjs bindings for Slate
- y-indexeddb - IndexedDB persistence provider
- Hocuspocus - Backend server for Yjs
- y-webrtc - WebRTC provider
- RemoteCursorOverlay - Remote cursor component
- EditorContainer - Editor container component
Plugins
YjsPlugin
Enables real-time collaboration using Yjs with support for multiple providers and remote cursors.
Array of provider configurations or pre-instantiated provider instances. The plugin will create instances from configurations and use existing instances directly. All providers will share the same Y.Doc and Awareness. Each configuration object specifies a provider type (for example 'indexeddb', 'hocuspocus',
or 'webrtc') and its specific options. Custom provider instances must conform to the
UnifiedProvider interface.
Configuration for remote cursors. Set to null to explicitly disable cursors. If omitted, cursors are enabled by default if providers are specified. Passed to withTCursors. See WithCursorsOptions API. Includes data for local user info and autoSend (default true).
Optional shared Y.Doc instance. If not provided, a new one will be created internally by the plugin. Provide your own if integrating with other Yjs tools or managing multiple documents.
Optional shared Awareness instance. If not provided, a new one will be created.
Callback fired when any provider successfully connects.
Callback fired when any provider disconnects.
Callback fired when any provider encounters an error (e.g., connection failure).
Callback fired when the sync status (provider.isSynced) of any individual provider changes.
API
api.yjs.init
Initializes the Yjs connection, binds it to the editor, sets up providers based on plugin configuration, potentially populates the Y.Doc with initial content, and connects providers. Must be called after the editor is mounted.
A unique identifier for the Yjs document (e.g., room name, document ID). If not provided, editor.id is used. Essential for ensuring collaborators connect to the same document state.
The initial content for the editor. This is only applied if the Y.Doc associated with the id is completely empty in the shared state (backend/peers). If the document already exists, its content will be synced, ignoring this value. Can be Plate JSON (Value), an HTML string, or a function returning/resolving to Value. If omitted or empty, a default empty paragraph is used for initialization if the Y.Doc is new.
Whether to automatically call provider.connect() for all configured providers during initialization. Default: true. Set to false if you want to manage connections manually using editor.api.yjs.connect().
If set, automatically focuses the editor and places the cursor at the 'start' or 'end' of the document after initialization and sync.
Specific Plate Location to set the selection to after initialization, overriding autoSelect.
api.yjs.destroy
Disconnects all providers, cleans up Yjs bindings (detaches editor from Y.Doc), and destroys the awareness instance. Must be called when the editor component unmounts to prevent memory leaks and stale connections.
api.yjs.connect
Manually connects to providers. Useful if autoConnect: false was used during init.
api.yjs.disconnect
Manually disconnects from providers.
On This Page
FeaturesUsageInstallationAdd PluginConfigure YjsPluginAdd Editor ContainerInitialize Yjs ConnectionMonitor Connection Status (Optional)Provider TypesHocuspocus ProviderwsOptionsWebRTC ProviderIndexedDB ProviderCustom ProviderBackend SetupIndexedDB Local PersistenceHocuspocus ServerWebRTC SetupSignaling ServerTURN ServersSecurityTroubleshootingConnection IssuesMultiple DocumentsSync IssuesCursor IssuesRelatedPluginsYjsPluginAPIapi.yjs.initapi.yjs.destroyapi.yjs.connectapi.yjs.disconnect