Skip to main content
The @veltdev/slate-crdt library enables real-time collaborative editing on Slate Editors. The collaboration editing engine is built on top of Yjs, @slate-yjs/core, and Velt SDK.

Prerequisites

  • Node.js (v14 or higher)
  • React (v18 or higher)
  • A Velt account with an API key (sign up)
  • Optional: TypeScript for type safety

Setup

Step 1: Install Dependencies

Install the required packages:

Step 2: Setup Velt

Initialize the Velt client by following the Velt Setup Docs. This is required for the collaboration engine to work.

Step 3: Initialize Collaborative Editor

  • Create and own the Slate editor in your application. The collaboration library enhances the editor you pass in.
  • Pass an editorId to uniquely identify each editor instance you have in your app.
  • Use Slate Descendant[] nodes for initialContent, not an HTML string.
Use useCollaboration() to manage Velt readiness, manager initialization, reactive status, versions, and cleanup.
The manager applies withYjs, withYHistory, and—unless disabled—withCursors to the consumer-owned editor. Create the editor once with useMemo() so the manager is not recreated on every render.

Step 4: Status Monitoring (Optional)

The React hook returns reactive status, isSynced, and error state. For event-style subscriptions, use the manager:
You can also read the current values directly:

Step 5: Version Management (Optional)

Save and restore named snapshots of the document. The React hook keeps versions in state and provides convenience methods.
The same methods are available on the manager:

Step 6: Force Reset Initial Content (Optional)

By default, initialContent is applied exactly once when the shared document is brand new. Pass Slate Descendant[] nodes to initialContent.
Use forceResetInitialContent only for deliberate reset workflows. It clears the current shared Slate content for every connected user.

Step 7: Configure Remote Cursors (Optional)

Slate cursor state is shared through Yjs awareness by withCursors. Pass local cursor metadata through cursorData, publish selection changes, and use @slate-yjs/react decorations to render remote cursors.
Update or inspect awareness-backed cursor data through the manager:
Set enableCursors: false when your application does not need awareness-backed remote selections.

Step 8: CRDT Event Subscription (Optional)

Listen for real-time CRDT data events using getCrdtElement().on() from the Velt client.

Step 9: Custom Encryption (Optional)

Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of type Uint8Array | number[].
See also: setEncryptionProvider() · VeltEncryptionProvider · EncryptConfig · DecryptConfig

Step 10: Error Handling (Optional)

Pass an onError callback to handle initialization or runtime errors. The React hook also exposes a reactive error state.

Step 11: Access Yjs Internals (Advanced)

The manager exposes escape hatches for direct Yjs manipulation when needed.

Step 12: Cleanup

The React hook destroys the manager automatically when the component unmounts or when the editorId, editor, or Velt client changes. It also returns a destroy() method for early cleanup.
When using the imperative API, call manager.destroy() during cleanup:

Notes

  • Consumer-owned editor: Create the Slate editor once and pass it to the hook or manager.
  • Plugin order: The manager applies withYjs, withYHistory, and withCursors before the store hydrates remote data.
  • Initial content shape: Pass Slate Descendant[] nodes, not HTML.
  • Enhanced editor: manager.getEditor() returns the editor after collaboration plugins are applied.
  • Remote cursors: Publish selection changes and render remote cursor decorations with @slate-yjs/react.
  • Unique editorId: Users must share the same Velt document context and editorId to collaborate.
  • Production authentication: Do not expose privileged server tokens in browser code; use Velt’s recommended server-backed authentication flow.

Testing and Debugging

To test collaboration:
  1. Log in as two unique users in two separate browser profiles and open the same Velt document and Slate editorId in both.
  2. Edit text or formatting in one window and verify the changes and remote cursor appear in the other.
  3. Make concurrent edits and verify the Slate-Yjs binding merges them without data loss.
  4. Reload both windows and verify the shared document persists.
Common issues:
  • Editor recreated repeatedly: Wrap withReact(createEditor()) in useMemo().
  • Editor not loading: Confirm the Velt client is initialized and the user is authenticated before creating the manager.
  • Cursors not appearing: Pass cursorData, publish the current selection, and use useDecorateRemoteCursors().
  • Initial content not rendering: Pass a valid Slate Descendant[] value.
  • Changes not syncing: Confirm both users have the same Velt document context and editorId.
Enable browser console warnings to see detailed debugging information. You can also use the VeltCrdtStoreMap debugging interface to inspect and monitor your CRDT stores in real time.

Complete Example

A complete collaborative Slate editor with user login, cursor decorations, status display, and version management.
Complete App.tsx

How It Works

  1. Your application creates the Slate editor and composes its normal React plugins.
  2. useCollaboration or createCollaboration receives that editor and creates a Velt CRDT store backed by a Yjs document and shared Y.XmlText.
  3. The manager applies withYjs, withYHistory, and withCursors before remote data hydrates, so no remote update is missed.
  4. Local Slate operations flow into the shared Y.XmlText, and remote operations flow back into the enhanced editor through @slate-yjs/core.
  5. Remote cursors and selections use Yjs awareness and render through @slate-yjs/react decorations.
  6. Initial content is applied only for a brand-new document unless force reset is enabled.
  7. Version management saves and restores named Yjs snapshots for all connected users.
  8. Cleanup disconnects the Slate-Yjs binding, destroys the undo manager, clears awareness, and destroys the store.

APIs

React: useCollaboration()

React lifecycle wrapper around createCollaboration(). Alias: useSlateCollaboration.
  • Signature: useCollaboration(config: UseCollaborationConfig): UseCollaborationReturn
  • Params:
    • editorId: Unique identifier for the collaborative editor.
    • editor: Consumer-owned Slate editor to enhance.
    • veltClient: Optional explicit Velt client. Falls back to VeltProvider context.
    • initialContent: Slate Descendant[] applied to a brand-new document.
    • cursorData: Local cursor name, colors, and custom awareness fields.
    • yjsOptions: Options forwarded to withYjs.
    • undoManagerOptions: Options forwarded to withYHistory.
    • cursorOptions: Options forwarded to withCursors.
    • enableCursors: Set to false to skip cursor integration. Default: true.
    • autoConnect: Set to false to leave the Yjs editor disconnected after initialization. Default: true.
    • debounceMs: Throttle interval in milliseconds for backend writes. Default: 0.
    • forceResetInitialContent: Replace existing shared content during initialization. Default: false.
    • onError: Callback for non-fatal collaboration errors.
  • Returns:
    • editor: Enhanced Slate editor, or null while loading.
    • manager: Underlying CollaborationManager, or null while loading.
    • isLoading: true until initialization completes.
    • synced: true after initial backend sync completes.
    • isSynced: Alias of synced.
    • status: 'connecting', 'connected', or 'disconnected'.
    • error: Most recent error, or null.
    • versions: Reactive saved-version list.
    • saveVersion: Save a named version and refresh the list.
    • restoreVersion: Restore a version by ID.
    • refreshVersions: Refresh the saved-version list.
    • destroy: Destroy collaboration early.

Imperative: createCollaboration()

Creates and initializes a manager. It reports failures through onError and resolves with a degraded manager rather than intentionally throwing into consumer code.
  • Signature: createCollaboration(config: CollaborationConfig): Promise<CollaborationManager>

EnhancedSlateEditor

The consumer-owned Slate Editor after YjsEditor, YHistoryEditor, and CursorEditor capabilities are applied.

SlateCursorData

Version

CollaborationManager Methods

manager.getEditor()

Get the enhanced Slate editor.
  • Returns: EnhancedSlateEditor | null

manager.onStatusChange()

Subscribe to connection status changes.
  • Signature: manager.onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe

manager.onSynced()

Subscribe to initial sync state changes.
  • Signature: manager.onSynced(callback: (synced: boolean) => void): Unsubscribe

manager.initialized

Whether initialization has completed.
  • Returns: boolean

manager.synced

Whether initial backend sync has completed.
  • Returns: boolean

manager.status

Current connection status.
  • Returns: SyncStatus ('connecting' | 'connected' | 'disconnected')

manager.updateCursorData()

Update the local user’s awareness metadata.
  • Signature: manager.updateCursorData(data: SlateCursorData): void

manager.sendCursorPosition()

Publish the current Slate selection through awareness.
  • Signature: manager.sendCursorPosition(range?: Range | null): void

manager.getCursorStates()

Read remote cursor states keyed by Yjs client ID.
  • Returns: Record<string, unknown>

manager.saveVersion()

Save a named snapshot and return its version ID.
  • Signature: manager.saveVersion(name: string): Promise<string>

manager.getVersions()

List all saved versions.
  • Returns: Promise<Version[]>

manager.getVersionById()

Get one version by ID.
  • Signature: manager.getVersionById(versionId: string): Promise<Version | null>

manager.restoreVersion()

Restore a version for all connected users.
  • Signature: manager.restoreVersion(versionId: string): Promise<boolean>

manager.setStateFromVersion()

Apply a Version object’s state to the shared document.
  • Signature: manager.setStateFromVersion(version: Version): Promise<void>

manager.getDoc()

Get the underlying Yjs document.
  • Returns: Y.Doc | null

manager.getXmlText()

Get the shared Y.XmlText bound to Slate content.
  • Returns: Y.XmlText | null

manager.getProvider()

Get the Velt sync provider.
  • Returns: SyncProvider | null

manager.getAwareness()

Get the Yjs awareness instance.
  • Returns: Awareness | null

manager.getUndoManager()

Get the collaborative undo manager created by withYHistory.
  • Returns: Y.UndoManager | null

manager.getStore()

Get the underlying Velt CRDT store.
  • Returns: Store<string> | null

manager.destroy()

Disconnect the Slate-Yjs binding, destroy the undo manager, clear awareness, remove listeners, and destroy the store.
  • Returns: void

Advanced Content Helpers

@veltdev/slate-crdt exports two helpers for advanced initial-content flows:
  • normalizeInitialContent(initialContent?: Descendant[]): Descendant[] returns a valid Slate document when input is missing or invalid.
  • applyInitialContent(doc, xmlText, initialContent?, force?): boolean idempotently writes initial Slate nodes to the shared Y.XmlText.

Custom Encryption

Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider on VeltProvider or the imperative Velt client.