{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "yjs-docs",
  "title": "Collaboration",
  "description": "Real-time collaboration with Yjs",
  "files": [
    {
      "path": "../../content/docs/(plugins)/(collaboration)/yjs.mdx",
      "content": "---\ntitle: Collaboration\ndescription: Real-time collaboration with Yjs\ntoc: true\n---\n\n<ComponentPreview name=\"collaboration-demo\" />\n\n<PackageInfo>\n\n## Features\n\n- **Multi-Provider Support:** Enables real-time collaboration using [Yjs](https://github.com/yjs/yjs) and [slate-yjs](https://docs.slate-yjs.dev/). Supports multiple synchronization providers simultaneously (e.g., IndexedDB + Hocuspocus + WebRTC) working on a shared `Y.Doc`.\n- **Built-in Providers:** Includes support for [IndexedDB](https://github.com/yjs/y-indexeddb) local persistence, [Hocuspocus](https://tiptap.dev/hocuspocus) server-based collaboration, and [WebRTC](https://github.com/yjs/y-webrtc) peer-to-peer collaboration.\n- **Custom Providers:** Extensible architecture allows adding custom providers by implementing the `UnifiedProvider` interface.\n- **Awareness & Cursors:** Integrates Yjs Awareness protocol for sharing cursor locations and other ephemeral state between users. Includes [`RemoteCursorOverlay`](/docs/components/remote-cursor-overlay) for rendering remote cursors.\n- **Customizable Cursors:** Cursor appearance (name, color) can be customized via `cursors`.\n- **Manual Lifecycle:** Provides explicit `init` and `destroy` methods for managing the Yjs connection lifecycle.\n\n</PackageInfo>\n\n## Usage\n\n<Steps>\n\n### Installation\n\nInstall the core Yjs plugin.\n\n```bash\nnpm install @platejs/yjs\n```\n\nFor Hocuspocus server-based collaboration:\n\n```bash\nnpm install @hocuspocus/provider\n```\n\nFor WebRTC peer-to-peer collaboration:\n\n```bash\nnpm install y-webrtc\n```\n\n### Add Plugin\n\n```tsx\nimport { YjsPlugin } from '@platejs/yjs/react';\nimport { createPlateEditor } from 'platejs/react';\n\nconst editor = createPlateEditor({\n  plugins: [\n    // ...otherPlugins,\n    YjsPlugin,\n  ],\n  // Important: Skip Plate's default initialization when using Yjs\n  skipInitialization: true,\n});\n```\n\n<Callout type=\"warning\" title=\"Required Editor Configuration\">\n  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.\n</Callout>\n\n### Configure YjsPlugin\n\nConfigure the plugin with providers and cursor settings:\n\n```tsx\nimport { YjsPlugin } from '@platejs/yjs/react';\nimport { createPlateEditor } from 'platejs/react';\nimport { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay';\n\nconst editor = createPlateEditor({\n  plugins: [\n    // ...otherPlugins,\n    YjsPlugin.configure({\n      render: {\n        afterEditable: RemoteCursorOverlay,\n      },\n      options: {\n        // Configure local user cursor appearance\n        cursors: {\n          data: {\n            name: 'User Name', // Replace with dynamic user name\n            color: '#aabbcc', // Replace with dynamic user color\n          },\n        },\n        // Configure providers. All providers share the same Y.Doc and Awareness instance.\n        providers: [\n          // Example: IndexedDB provider for local persistence\n          {\n            type: 'indexeddb',\n            options: {\n              docName: 'my-document-id', // Unique IndexedDB database name\n            },\n          },\n          // Example: Hocuspocus provider\n          {\n            type: 'hocuspocus',\n            options: {\n              name: 'my-document-id', // Unique identifier for the document\n              url: 'ws://localhost:8888', // Your Hocuspocus server URL\n            },\n          },\n          // Example: WebRTC provider (can be used alongside Hocuspocus)\n          {\n            type: 'webrtc',\n            options: {\n              roomName: 'my-document-id', // Must match the document identifier\n              signaling: ['ws://localhost:4444'], // Optional: Your signaling server URLs\n            },\n          },\n        ],\n      },\n    }),\n  ],\n  skipInitialization: true,\n});\n```\n\n- `render.afterEditable`: Assigns [`RemoteCursorOverlay`](/docs/components/remote-cursor-overlay) to render remote user cursors.\n- `cursors.data`: Configures the local user's cursor appearance with name and color.\n- `providers`: Array of collaboration providers to use (Hocuspocus, WebRTC, or custom providers).\n\n### Add Editor Container\n\nThe `RemoteCursorOverlay` requires a positioned container around the editor content. Use [`EditorContainer`](/docs/components/editor) component or `PlateContainer` from `platejs/react`:\n\n```tsx\nimport { Plate } from 'platejs/react';\nimport { EditorContainer } from '@/components/ui/editor';\n\nreturn (\n  <Plate editor={editor}>\n    <EditorContainer>\n      <Editor />\n    </EditorContainer>\n  </Plate>\n);\n```\n\n### Initialize Yjs Connection\n\nYjs connection and state initialization are handled manually, typically within a `useEffect` hook:\n\n```tsx\nimport React, { useEffect } from 'react';\nimport { YjsPlugin } from '@platejs/yjs/react';\nimport { useMounted } from '@/hooks/use-mounted'; // Or your own mounted check\n\nconst MyEditorComponent = ({ documentId, initialValue }) => {\n  const editor = usePlateEditor(/** editor config from previous steps **/);\n  const mounted = useMounted();\n\n  useEffect(() => {\n    // Ensure component is mounted and editor is ready\n    if (!mounted) return;\n\n    // Initialize Yjs connection, sync document, and set initial editor state\n    editor.getApi(YjsPlugin).yjs.init({\n      id: documentId,          // Unique identifier for the Yjs document\n      value: initialValue,     // Initial content if the Y.Doc is empty\n    });\n\n    // Clean up: Destroy connection when component unmounts\n    return () => {\n      editor.getApi(YjsPlugin).yjs.destroy();\n    };\n  }, [editor, mounted]);\n\n  return (\n    <Plate editor={editor}>\n      <EditorContainer>\n        <Editor />\n      </EditorContainer>\n    </Plate>\n  );\n};\n```\n\n<Callout>\n  **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.\n  \n  **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.\n</Callout>\n\n### Monitor Connection Status (Optional)\n\nAccess provider states and add event handlers for connection monitoring:\n\n```tsx\nimport React from 'react';\nimport { YjsPlugin } from '@platejs/yjs/react';\nimport { usePluginOption } from 'platejs/react';\n\nfunction EditorStatus() {\n  // Access provider states directly (read-only)\n  const providers = usePluginOption(YjsPlugin, '_providers');\n  const isConnected = usePluginOption(YjsPlugin, '_isConnected');\n\n  return (\n    <div>\n      {providers.map((provider) => (\n        <span key={provider.type}>\n          {provider.type}: {provider.isConnected ? 'Connected' : 'Disconnected'} ({provider.isSynced ? 'Synced' : 'Syncing'})\n        </span>\n      ))}\n    </div>\n  );\n}\n\n// Add event handlers for connection events:\nYjsPlugin.configure({\n  options: {\n    // ... other options\n    onConnect: ({ type }) => console.debug(`Provider ${type} connected!`),\n    onDisconnect: ({ type }) => console.debug(`Provider ${type} disconnected.`),\n    onSyncChange: ({ type, isSynced }) => console.debug(`Provider ${type} sync status: ${isSynced}`),\n    onError: ({ type, error }) => console.error(`Error in provider ${type}:`, error),\n  },\n});\n```\n\n</Steps>\n\n## Provider Types\n\n### Hocuspocus Provider\n\nServer-based collaboration using [Hocuspocus](https://tiptap.dev/hocuspocus). Requires a running Hocuspocus server.\n\n```tsx\ntype HocuspocusProviderConfig = {\n  type: 'hocuspocus',\n  options: {\n    name: string;     // Document identifier\n    url: string;      // WebSocket server URL\n    token?: string;   // Authentication token\n    wsOptions?: HocuspocusProviderWebsocketConfiguration; // Advanced websocket config (headers, protocols, etc.)\n  }\n}\n```\n\n#### `wsOptions`\n\nYou 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`](https://tiptap.dev/hocuspocus/api/provider#websocket-configuration).\n\nExample usage:\n\n```tsx\n{\n  type: 'hocuspocus',\n  options: {\n    name: 'my-document-id',\n  },\n  wsOptions: {\n    url: 'ws://localhost:8888',\n    maxAttempts: 5,\n    parameters: {\n      // request parameters\n    }\n  },\n}\n```\n\n### WebRTC Provider\n\nPeer-to-peer collaboration using [y-webrtc](https://github.com/yjs/y-webrtc).\n\n```tsx\ntype WebRTCProviderConfig = {\n  type: 'webrtc',\n  options: {\n    roomName: string;      // Room name for collaboration\n    signaling?: string[];  // Signaling server URLs\n    password?: string;     // Room password\n    maxConns?: number;     // Max connections\n    peerOpts?: object;     // WebRTC peer options\n  }\n}\n```\n\n### IndexedDB Provider\n\nLocal browser persistence using [y-indexeddb](https://github.com/yjs/y-indexeddb). Use it alongside a network provider when the editor should restore local state before remote sync finishes.\n\n```tsx\ntype IndexeddbProviderConfig = {\n  type: 'indexeddb',\n  options: {\n    docName: string; // Stable IndexedDB database name for this document\n  }\n}\n```\n\n### Custom Provider\n\nCreate custom providers by implementing the `UnifiedProvider` interface:\n\n```typescript\ninterface UnifiedProvider {\n  awareness: Awareness;\n  document: Y.Doc;\n  type: string;\n  connect: () => void;\n  destroy: () => void;\n  disconnect: () => void;\n  isConnected: boolean;\n  isSynced: boolean;\n}\n```\n\nUse custom providers directly in the providers array:\n\n```tsx\nconst customProvider = new MyCustomProvider({ doc: ydoc, awareness });\n\nYjsPlugin.configure({\n  options: {\n    providers: [customProvider],\n  },\n});\n```\n\n## Backend Setup\n\n### IndexedDB Local Persistence\n\nIndexedDB 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:\n\n```tsx\n{\n  type: 'indexeddb',\n  options: {\n    docName: 'document-1',\n  },\n}\n```\n\nIndexedDB does not transport remote awareness or cursors. It persists document updates locally; Hocuspocus or WebRTC still own multi-user transport.\n\n### Hocuspocus Server\n\nSet up a [Hocuspocus server](https://tiptap.dev/hocuspocus/getting-started) for server-based collaboration. Ensure the `url` and `name` in your provider options match your server configuration.\n\n### WebRTC Setup\n\n#### Signaling Server\n\nWebRTC requires signaling servers for peer discovery. Public servers work for testing but use your own for production:\n\n```bash\nnpm install y-webrtc\nPORT=4444 node ./node_modules/y-webrtc/bin/server.js\n```\n\nConfigure your client to use custom signaling:\n\n```tsx\n{\n  type: 'webrtc',\n  options: {\n    roomName: 'document-1',\n    signaling: ['ws://your-signaling-server.com:4444'],\n  },\n}\n```\n\n#### TURN Servers\n\n<Callout type=\"warning\">\n  WebRTC connections can fail due to firewalls. Use TURN servers or combine with Hocuspocus for production reliability.\n</Callout>\n\nConfigure TURN servers for reliable connections:\n\n```tsx\n{\n  type: 'webrtc',\n  options: {\n    roomName: 'document-1',\n    signaling: ['ws://your-signaling-server.com:4444'],\n    peerOpts: {\n      config: {\n        iceServers: [\n          { urls: 'stun:stun.l.google.com:19302' },\n          {\n            urls: 'turn:your-turn-server.com:3478',\n            username: 'username',\n            credential: 'password'\n          }\n        ]\n      }\n    }\n  }\n}\n```\n\n## Security\n\n**Authentication & Authorization:**\n- Use Hocuspocus's `onAuthenticate` hook to validate users\n- Implement document-level access control on your backend\n- Pass authentication tokens via the `token` option\n\n**Transport Security:**\n- Use `wss://` URLs in production for encrypted communication\n- Configure secure TURN servers with the `turns://` protocol\n\n**WebRTC Security:**\n- Use the `password` option for basic room access control\n- Configure secure signaling servers\n\nExample secure configuration:\n\n```tsx\nYjsPlugin.configure({\n  options: {\n    providers: [\n      {\n        type: 'hocuspocus',\n        options: {\n          name: 'secure-document-id',\n          url: 'wss://your-hocuspocus-server.com',\n          token: 'user-auth-token',\n        },\n      },\n      {\n        type: 'webrtc',\n        options: {\n          roomName: 'secure-document-id',\n          password: 'strong-room-password',\n          signaling: ['wss://your-secure-signaling.com'],\n          peerOpts: {\n            config: {\n              iceServers: [\n                {\n                  urls: 'turns:your-turn-server.com:443?transport=tcp',\n                  username: 'user',\n                  credential: 'pass'\n                }\n              ]\n            }\n          }\n        },\n      },\n    ],\n  },\n});\n```\n\n## Troubleshooting\n\n### Connection Issues\n\n**Check URLs and Names:**\n- Verify `url` (Hocuspocus) and `signaling` URLs (WebRTC) are correct\n- Ensure `docName`, `name`, or `roomName` matches exactly across providers that share one document\n- Use `ws://` for local development, `wss://` for production\n\n**Server Status:**\n- Verify Hocuspocus and signaling servers are running\n- Check server logs for errors\n- Test TURN server connectivity if using WebRTC\n\n**Network Issues:**\n- Firewalls may block WebSocket or WebRTC traffic\n- Use TURN servers configured for TCP (port 443) for better traversal\n- Check browser console for provider errors\n\n### Multiple Documents\n\n**Separate Instances:**\n- Create separate `Y.Doc` instances for each document\n- Use unique document identifiers for `name`/`roomName`\n- Pass unique `ydoc` and `awareness` instances to each editor\n\n### Sync Issues\n\n**Editor Initialization:**\n- Always set `skipInitialization: true` when creating the editor\n- Use `editor.api.yjs.init({ value })` for initial content\n- Ensure all providers use the exact same document identifier\n\n**Content Conflicts:**\n- Avoid manually manipulating the shared `Y.Doc`\n- Let Yjs handle all document operations through the editor\n\n### Cursor Issues\n\n**Overlay Setup:**\n- Include [`RemoteCursorOverlay`](/docs/components/remote-cursor-overlay) in plugin render config\n- Use positioned container (`EditorContainer` or `PlateContainer`)\n- Verify `cursors.data` (name, color) is set correctly for local user\n\n## Related\n\n- [Yjs](https://github.com/yjs/yjs) - CRDT framework for collaboration\n- [slate-yjs](https://docs.slate-yjs.dev/) - Yjs bindings for Slate\n- [y-indexeddb](https://github.com/yjs/y-indexeddb) - IndexedDB persistence provider\n- [Hocuspocus](https://tiptap.dev/hocuspocus) - Backend server for Yjs\n- [y-webrtc](https://github.com/yjs/y-webrtc) - WebRTC provider\n- [RemoteCursorOverlay](/docs/components/remote-cursor-overlay) - Remote cursor component\n- [EditorContainer](/docs/components/editor) - Editor container component\n\n## Plugins\n\n### `YjsPlugin`\n\nEnables real-time collaboration using Yjs with support for multiple providers and remote cursors.\n\n<API name=\"YjsPlugin\">\n<APIOptions>\n  <APIItem name=\"providers\" type=\"(UnifiedProvider | YjsProviderConfig)[]\">\n    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'`,\n    or `'webrtc'`) and its specific `options`. Custom provider instances must conform to the\n    `UnifiedProvider` interface.\n  </APIItem>\n  <APIItem name=\"cursors\" type=\"WithCursorsOptions | null\" optional>\n    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](https://docs.slate-yjs.dev/api/slate-yjs-core/cursor-plugin#withcursors). Includes `data` for local user info and `autoSend` (default `true`).\n  </APIItem>\n  <APIItem name=\"ydoc\" type=\"Y.Doc\" optional>\n    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.\n  </APIItem>\n  <APIItem name=\"awareness\" type=\"Awareness\" optional>\n    Optional shared Awareness instance. If not provided, a new one will be created.\n  </APIItem>\n  <APIItem name=\"onConnect\" type=\"(props: { type: YjsProviderType }) => void\" optional>\n    Callback fired when any provider successfully connects.\n  </APIItem>\n  <APIItem name=\"onDisconnect\" type=\"(props: { type: YjsProviderType }) => void\" optional>\n    Callback fired when any provider disconnects.\n  </APIItem>\n  <APIItem name=\"onError\" type=\"(props: { error: Error; type: YjsProviderType }) => void\" optional>\n    Callback fired when any provider encounters an error (e.g., connection failure).\n  </APIItem>\n  <APIItem name=\"onSyncChange\" type=\"(props: { isSynced: boolean; type: YjsProviderType }) => void\" optional>\n    Callback fired when the sync status (`provider.isSynced`) of any individual provider changes.\n  </APIItem>\n</APIOptions>\n<APIAttributes>\n  {/* Attributes are internal state, generally use options or event handlers instead */}\n  <APIItem name=\"_isConnected\" type=\"boolean\">\n    Internal state: Whether at least one provider is currently connected.\n  </APIItem>\n  <APIItem name=\"_isSynced\" type=\"boolean\">\n    Internal state: Reflects overall sync status.\n  </APIItem>\n  <APIItem name=\"_providers\" type=\"UnifiedProvider[]\">\n    Internal state: Array of all active, instantiated provider instances.\n  </APIItem>\n</APIAttributes>\n</API>\n\n## API\n\n### `api.yjs.init`\n\nInitializes 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.**\n\n<API name=\"editor.api.yjs.init\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"object\" optional>\n    Configuration object for initialization.\n  </APIItem>\n</APIParameters>\n\n<APIOptions type=\"object\">\n  <APIItem name=\"id\" type=\"string\" optional>\n    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.\n  </APIItem>\n  <APIItem name=\"value\" type=\"Value | string | ((editor: PlateEditor) => Value | Promise<Value>)\" optional>\n    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.\n  </APIItem>\n  <APIItem name=\"autoConnect\" type=\"boolean\" optional>\n    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()`.\n  </APIItem>\n  <APIItem name=\"autoSelect\" type=\"'start' | 'end'\" optional>\n    If set, automatically focuses the editor and places the cursor at the 'start' or 'end' of the document after initialization and sync.\n  </APIItem>\n  <APIItem name=\"selection\" type=\"Location\" optional>\n    Specific Plate `Location` to set the selection to after initialization, overriding `autoSelect`.\n  </APIItem>\n</APIOptions>\n\n<APIReturns type=\"Promise<void>\">\n  Resolves when the initial setup (including potential async `value` resolution and YjsEditor binding) is complete. Note that provider connection and synchronization happen asynchronously.\n</APIReturns>\n</API>\n\n### `api.yjs.destroy`\n\nDisconnects 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.\n\n### `api.yjs.connect`\n\nManually connects to providers. Useful if `autoConnect: false` was used during `init`.\n\n<API name=\"editor.api.yjs.connect\">\n<APIParameters>\n <APIItem name=\"type\" type=\"YjsProviderType | YjsProviderType[]\" optional>\n   If provided, only connects to providers of the specified type(s). If omitted, connects to all configured providers that are not already connected.\n </APIItem>\n</APIParameters>\n</API>\n\n### `api.yjs.disconnect`\n\nManually disconnects from providers.\n\n<API name=\"editor.api.yjs.disconnect\">\n<APIParameters>\n <APIItem name=\"type\" type=\"YjsProviderType | YjsProviderType[]\" optional>\n   If provided, only disconnects from providers of the specified type(s). If omitted, disconnects from all currently connected providers.\n </APIItem>\n</APIParameters>\n</API>\n",
      "type": "registry:file",
      "target": "content/docs/plate/(plugins)/(collaboration)/yjs.mdx"
    }
  ],
  "type": "registry:file"
}