@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
editorIdto uniquely identify each editor instance you have in your app. - Use Slate
Descendant[]nodes forinitialContent, not an HTML string.
- React Hook
- Imperative API
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 reactivestatus, isSynced, and error state. For event-style subscriptions, use the manager:
Step 5: Version Management (Optional)
Save and restore named snapshots of the document. The React hook keepsversions in state and provides convenience methods.
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.
Step 7: Configure Remote Cursors (Optional)
Slate cursor state is shared through Yjs awareness bywithCursors. Pass local cursor metadata through cursorData, publish selection changes, and use @slate-yjs/react decorations to render remote cursors.
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 usinggetCrdtElement().on() from the Velt client.
- React / Next.js
- Imperative API
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 typeUint8Array | number[].
- React / Next.js
- Imperative API
Step 10: Error Handling (Optional)
Pass anonError 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 theeditorId, editor, or Velt client changes. It also returns a destroy() method for early cleanup.
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, andwithCursorsbefore 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
editorIdto 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:- Log in as two unique users in two separate browser profiles and open the same Velt document and Slate
editorIdin both. - Edit text or formatting in one window and verify the changes and remote cursor appear in the other.
- Make concurrent edits and verify the Slate-Yjs binding merges them without data loss.
- Reload both windows and verify the shared document persists.
- Editor recreated repeatedly: Wrap
withReact(createEditor())inuseMemo(). - 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 useuseDecorateRemoteCursors(). - Initial content not rendering: Pass a valid Slate
Descendant[]value. - Changes not syncing: Confirm both users have the same Velt document context and
editorId.
Complete Example
- React / Next.js
A complete collaborative Slate editor with user login, cursor decorations, status display, and version management.
Complete App.tsx
How It Works
- Your application creates the Slate editor and composes its normal React plugins.
useCollaborationorcreateCollaborationreceives that editor and creates a Velt CRDT store backed by a Yjs document and sharedY.XmlText.- The manager applies
withYjs,withYHistory, andwithCursorsbefore remote data hydrates, so no remote update is missed. - Local Slate operations flow into the shared
Y.XmlText, and remote operations flow back into the enhanced editor through@slate-yjs/core. - Remote cursors and selections use Yjs awareness and render through
@slate-yjs/reactdecorations. - Initial content is applied only for a brand-new document unless force reset is enabled.
- Version management saves and restores named Yjs snapshots for all connected users.
- Cleanup disconnects the Slate-Yjs binding, destroys the undo manager, clears awareness, and destroys the store.
APIs
React: useCollaboration()
React lifecycle wrapper aroundcreateCollaboration(). 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 toVeltProvidercontext.initialContent: SlateDescendant[]applied to a brand-new document.cursorData: Local cursor name, colors, and custom awareness fields.yjsOptions: Options forwarded towithYjs.undoManagerOptions: Options forwarded towithYHistory.cursorOptions: Options forwarded towithCursors.enableCursors: Set tofalseto skip cursor integration. Default:true.autoConnect: Set tofalseto 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, ornullwhile loading.manager: UnderlyingCollaborationManager, ornullwhile loading.isLoading:trueuntil initialization completes.synced:trueafter initial backend sync completes.isSynced: Alias ofsynced.status:'connecting','connected', or'disconnected'.error: Most recent error, ornull.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 throughonError and resolves with a degraded manager rather than intentionally throwing into consumer code.
- Signature:
createCollaboration(config: CollaborationConfig): Promise<CollaborationManager>
EnhancedSlateEditor
The consumer-owned SlateEditor 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 aVersion 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 sharedY.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 bywithYHistory.
- 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?): booleanidempotently writes initial Slate nodes to the sharedY.XmlText.
Custom Encryption
Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider onVeltProvider or the imperative Velt client.
