The @veltdev/tiptap-crdt-react and @veltdev/tiptap-crdt libraries enable real-time collaborative editing on Tiptap Editors. The collaboration editing engine is built on top of Yjs and Velt SDK.
Prerequisites
Node.js (v14 or higher)
React (v16.8 or higher for hooks)
A Velt account with an API key (sign up )
Optional: TypeScript for type safety
Setup
Step 1: Install Dependencies
Install the required packages:
React / Next.js
Other Frameworks
npm install @veltdev/tiptap-crdt-react @veltdev/tiptap-crdt @veltdev/react @veltdev/types @tiptap/core @tiptap/starter-kit yjs
npm install @veltdev/tiptap-crdt @veltdev/client @tiptap/core @tiptap/starter-kit yjs
Step 2: Setup Velt
Initialize the Velt client and set the document context. This is required for the collaboration engine to work.
React / Next.js
Other Frameworks
Wrap your app with VeltProvider, identify the user, and use useSetDocument to scope the collaborative session. import { VeltProvider , useSetDocument , useVeltClient } from '@veltdev/react' ;
function App () {
return (
< VeltProvider apiKey = "YOUR_API_KEY" >
< YourApp />
</ VeltProvider >
);
}
function YourApp () {
const { client } = useVeltClient ();
useEffect (() => {
if ( ! client ) return ;
client . identify ({
userId: 'user-1' ,
name: 'Alice' ,
email: 'alice@example.com' ,
organizationId: 'my-org' ,
});
}, [ client ]);
useSetDocument ( 'my-tiptap-editor' , { documentName: 'My Document' });
return < TipTapEditor /> ;
}
Initialize the Velt SDK with initVelt(), set the document context, and authenticate a user. import { initVelt } from '@veltdev/client' ;
const client = await initVelt ( 'YOUR_API_KEY' );
// Set the document scope
client . setDocument ( 'my-document-id' , { documentName: 'My Document' });
// Authenticate a user
await client . identify ({
userId: 'user-1' ,
name: 'John Doe' ,
email: 'john@example.com' ,
});
Step 3: Initialize Collaborative Editor
Initialize the collaboration manager and use it to create the Tiptap editor.
Pass an editorId to uniquely identify each editor instance you have in your app. This is especially important when you have multiple editors in your app.
React / Next.js
Other Frameworks
Use the useCollaboration hook to manage the entire CRDT lifecycle. It creates a CollaborationManager, initializes the CRDT store, and returns a TipTap Extension for collaboration and cursor tracking. import { useCollaboration } from '@veltdev/tiptap-crdt-react' ;
import { Editor } from '@tiptap/core' ;
import StarterKit from '@tiptap/starter-kit' ;
import { useEffect , useRef } from 'react' ;
function TipTapEditor () {
const editorElRef = useRef < HTMLDivElement >( null );
const { extension , isLoading , isSynced , status , error , manager } = useCollaboration ({
editorId: 'my-tiptap-editor' ,
initialContent: '<p>Start typing here...</p>' ,
onError : ( err ) => console . error ( 'Collaboration error:' , err ),
});
useEffect (() => {
if ( ! extension || ! editorElRef . current ) return ;
const editor = new Editor ({
element: editorElRef . current ,
extensions: [
StarterKit . configure ({ undoRedo: false }),
extension ,
],
content: '' ,
});
return () => editor . destroy ();
}, [ extension ]);
if ( error ) return < div > Error: { error . message } </ div > ;
if ( isLoading || ! extension ) return < div > Connecting... </ div > ;
return < div ref = { editorElRef } /> ;
}
Disable TipTap’s built-in undo/redo (undoRedo: false in StarterKit), CRDT manages document state.
Use createCollaboration() to create a CollaborationManager, then call manager.createExtension() to get a single TipTap extension that bundles both Yjs document binding and remote cursor rendering. import { createCollaboration } from '@veltdev/tiptap-crdt' ;
import { Editor } from '@tiptap/core' ;
import StarterKit from '@tiptap/starter-kit' ;
// Wait for the SDK to be ready
client . getVeltInitState (). subscribe ( async ( isReady ) => {
if ( ! isReady ) return ;
// Create the CollaborationManager
const manager = await createCollaboration ({
editorId: 'my-document-id' ,
veltClient: client ,
initialContent: '<p>Start typing here...</p>' ,
onError : ( error ) => console . error ( 'Collaboration error:' , error ),
});
// Create TipTap editor with the collaboration extension
const editor = new Editor ({
element: document . querySelector ( '#editor' ),
extensions: [
StarterKit . configure ({ undoRedo: false }),
manager . createExtension (),
],
content: '' , // Content comes from CRDT store
});
});
Disable TipTap’s built-in undo/redo (undoRedo: false in StarterKit), CRDT manages document state.
Step 4: Add CSS for Collaboration Cursor
Add the following CSS to your app to style the collaboration cursor.
/* Collaboration cursor styling */
.collaboration-cursor__caret ,
.collaboration-carets__caret {
border-left : 1 px solid #0d0d0d ;
border-right : 1 px solid #0d0d0d ;
margin-left : -1 px ;
margin-right : -1 px ;
pointer-events : none ;
position : relative ;
word-break : normal ;
}
/* Render the username above the caret */
.collaboration-cursor__label ,
.collaboration-carets__label {
border-radius : 3 px 3 px 3 px 0 ;
color : #0d0d0d ;
font-size : 12 px ;
font-style : normal ;
font-weight : 600 ;
left : -1 px ;
line-height : normal ;
padding : 0.1 rem 0.3 rem ;
position : absolute ;
top : -1.4 em ;
user-select : none ;
white-space : nowrap ;
}
Step 5: Status Monitoring (Optional)
Monitor the connection status and sync state to display UI indicators.
React / Next.js
Other Frameworks
The useCollaboration hook returns reactive status, isSynced, and error state: const { extension , isLoading , isSynced , status , error } = useCollaboration ({
editorId: 'my-tiptap-editor' ,
});
if ( error ) return < div > Error: { error . message } </ div > ;
if ( isLoading ) return < div > Connecting... </ div > ;
// In your JSX
< div > Status: { status } | Synced: { isSynced ? 'Yes' : 'No' } </ div >
Subscribe to status and sync changes on the manager: // Connection status: 'connecting' | 'connected' | 'disconnected'
manager . onStatusChange (( status ) => {
console . log ( 'Status:' , status );
});
// Sync state: true when initial sync is complete
manager . onSynced (( synced ) => {
console . log ( 'Synced:' , synced );
});
// Read current values at any time
console . log ( manager . status ); // 'connected'
console . log ( manager . synced ); // true
console . log ( manager . initialized ); // true
Step 6: Version Management (Optional)
Save and restore named snapshots of the document state.
React / Next.js
Other Frameworks
The manager returned by useCollaboration provides version management methods: // Save a version
const versionId = await manager . saveVersion ( 'Before major edit' );
// List all versions
const versions = await manager . getVersions ();
// Restore a specific version
await manager . restoreVersion ( versions [ 0 ]. versionId );
// Save a named snapshot
const versionId = await manager . saveVersion ( 'Draft v1' );
// List all saved versions
const versions = await manager . getVersions ();
// Returns: [{ versionId, versionName, timestamp }, ...]
// Restore to a saved version
await manager . restoreVersion ( versionId );
Step 7: Force Reset Initial Content (Optional)
By default, initialContent is applied exactly once, only when the document is brand new. Pass forceResetInitialContent: true to always overwrite remote data with initialContent on initialization. Useful for “reset to template” workflows.
React / Next.js
Other Frameworks
const collab = useCollaboration ({
editorId: 'my-tiptap-editor' ,
initialContent: '<p>Fresh start!</p>' ,
forceResetInitialContent: true ,
});
const manager = await createCollaboration ({
editorId: 'my-document-id' ,
veltClient: client ,
initialContent: '<p>Fresh start!</p>' ,
forceResetInitialContent: true ,
});
Step 8: CRDT Event Subscription (Optional)
Listen for real-time sync events using getCrdtElement().on() from the Velt client.
React / Next.js
Other Frameworks
import { useVeltClient } from '@veltdev/react' ;
const { client } = useVeltClient ();
useEffect (() => {
if ( ! client ) return ;
const crdtElement = client . getCrdtElement ();
const sub = crdtElement . on ( 'updateData' ). subscribe (( event ) => {
if ( event ) console . log ( 'CRDT update:' , event );
});
return () => sub ?. unsubscribe ?.();
}, [ client ]);
const crdtElement = client . getCrdtElement ();
crdtElement . on ( 'updateData' ). subscribe (( event ) => {
if ( event ) {
console . log ( 'CRDT data updated:' , event );
}
});
Step 9: Custom Encryption (Optional)
Encrypt CRDT data before it’s stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of type Uint8Array | number[].
React / Next.js
Other Frameworks
const encryptionProvider = {
encrypt : async ({ data }) => data . map (( n ) => n . toString ()). join ( '__' ),
decrypt : async ({ data }) => data . split ( '__' ). map (( s ) => parseInt ( s , 10 )),
};
< VeltProvider apiKey = "YOUR_API_KEY" encryptionProvider = { encryptionProvider } >
...
</ VeltProvider >
const encryptionProvider = {
encrypt : async ( config ) => {
return config . data . map (( num ) => num . toString ()). join ( '__' );
},
decrypt : async ( config ) => {
return config . data . split ( '__' ). map (( str ) => parseInt ( str , 10 ));
},
};
client . setEncryptionProvider ( encryptionProvider );
See also: setEncryptionProvider()
· VeltEncryptionProvider
· EncryptConfig
· DecryptConfig
Step 10: Error Handling (Optional)
React / Next.js
Other Frameworks
Pass an onError callback to handle initialization or runtime errors. The error reactive state is also available for rendering. const collab = useCollaboration ({
editorId: 'my-tiptap-editor' ,
onError : ( error ) => console . error ( 'Collaboration error:' , error ),
});
if ( collab . error ) {
return < div > Error: { collab . error . message } </ div > ;
}
Pass an onError callback when creating the CollaborationManager: const manager = await createCollaboration ({
editorId: 'my-document-id' ,
veltClient: client ,
onError : ( error ) => console . error ( 'Collaboration error:' , error ),
});
Step 11: Access Yjs Internals (Advanced)
The manager exposes escape hatches for direct Yjs manipulation when needed.
React / Next.js
Other Frameworks
// From the manager returned by useCollaboration
const doc = manager . getDoc ();
const xml = manager . getXmlFragment ();
const awareness = manager . getAwareness ();
const provider = manager . getProvider ();
const store = manager . getStore ();
import * as Y from 'yjs' ;
const doc = manager . getDoc ();
const xml = manager . getXmlFragment ();
const provider = manager . getProvider ();
const awareness = manager . getAwareness ();
const store = manager . getStore ();
Step 12: Cleanup
Cleanup is automatic, when the TipTap editor is destroyed, the extension’s onDestroy hook triggers manager.destroy(), which cascades cleanup to the store, provider, and all listeners.
You can also call manager.destroy() manually:
React / Next.js
Other Frameworks
// Automatic on component unmount via useEffect cleanup
// Or manually:
manager . destroy ();
Notes
Unique editorId : Use a unique editorId per editor instance.
Disable history : Turn off Tiptap history / undoRedo when using collaboration.
Auth required : Ensure the Velt client is initialized and user is available before starting.
Testing and Debugging
To test collaboration:
Login two unique users on two browser profiles and open the same page in both.
Edit in one window and verify changes and cursors appear in the other.
Common issues:
Cursors not appearing: Ensure each editor has a unique editorId and users are authenticated. Also ensure that you are logged in with two different users in two different browser profiles.
Editor not loading: Confirm the Velt client is initialized and the API key is valid.
Desynced content: Make sure Tiptap history / undoRedo is disabled when using collaboration.
Enable browser console warnings to see detailed debugging information. The Velt SDK logs helpful warnings and errors to the console that can help you troubleshoot issues quickly. You can also use the VeltCrdtStoreMap debugging interface to inspect and monitor your CRDT stores in real-time.
Complete Example
A complete collaborative TipTap editor with user login, status display, and version management. App.tsx import { useMemo } from 'react' ;
import { VeltProvider , useSetDocument , useCurrentUser } from '@veltdev/react' ;
import { Header } from './Header' ;
import { EditorComponent } from './Editor' ;
const API_KEY = 'YOUR_API_KEY' ;
function getDocumentParams () {
const params = new URLSearchParams ( window . location . search );
return {
documentId: params . get ( 'documentId' ) || 'tiptap-crdt-react-demo-doc-1' ,
documentName: params . get ( 'documentName' ) || 'TipTap CRDT React Demo' ,
};
}
function AppContent () {
const veltUser = useCurrentUser ();
const { documentId , documentName } = useMemo (() => getDocumentParams (), []);
useSetDocument ( documentId , { documentName });
return (
< div className = "app-container" >
< Header />
< main className = "app-content" >
< EditorComponent key = { veltUser ?. userId || '__none__' } />
</ main >
</ div >
);
}
export const App = () => (
< VeltProvider apiKey = { API_KEY } >
< AppContent />
</ VeltProvider >
);
See all 35 lines
Editor.tsx import { useEffect , useRef , useState , useCallback } from 'react' ;
import { useVeltClient , useCurrentUser } from '@veltdev/react' ;
import { useCollaboration } from '@veltdev/tiptap-crdt-react' ;
import { Editor } from '@tiptap/core' ;
import StarterKit from '@tiptap/starter-kit' ;
import type { Version } from '@veltdev/crdt' ;
const DEFAULT_INITIAL_CONTENT = '<p>Hello TipTap CRDT!</p>' ;
export const EditorComponent = () => {
const editorElRef = useRef < HTMLDivElement >( null );
const editorInstanceRef = useRef < Editor | null >( null );
const [ versions , setVersions ] = useState < Version []>([]);
const [ versionInput , setVersionInput ] = useState ( '' );
const veltUser = useCurrentUser ();
const { extension , isLoading , isSynced , status , error , manager } = useCollaboration ({
editorId: 'my-tiptap-editor' ,
initialContent: DEFAULT_INITIAL_CONTENT ,
onError : ( err ) => console . error ( 'Collaboration error:' , err ),
});
// Create TipTap Editor when extension is ready
useEffect (() => {
if ( ! extension || ! editorElRef . current || editorInstanceRef . current ) return ;
const editor = new Editor ({
element: editorElRef . current ,
extensions: [
StarterKit . configure ({ undoRedo: false }),
extension ,
],
content: '' ,
});
editorInstanceRef . current = editor ;
return () => {
editor . destroy ();
editorInstanceRef . current = null ;
};
}, [ extension ]);
// Version management
const refreshVersions = useCallback ( async () => {
if ( ! manager ) return ;
setVersions ( await manager . getVersions ());
}, [ manager ]);
useEffect (() => { refreshVersions (); }, [ manager , refreshVersions ]);
const handleSaveVersion = async ( e : React . FormEvent ) => {
e . preventDefault ();
if ( ! manager || ! versionInput . trim ()) return ;
await manager . saveVersion ( versionInput . trim ());
setVersionInput ( '' );
await refreshVersions ();
};
const handleRestoreVersion = async ( versionId : string ) => {
if ( ! manager ) return ;
await manager . restoreVersion ( versionId );
await refreshVersions ();
};
if ( error ) return < div > Error: { error . message } </ div > ;
if ( isLoading || ! extension ) return < div > Connecting... </ div > ;
return (
< div className = "editor-container" >
< div id = "editor-header" >
{ veltUser ? `Editing as ${ veltUser . name } ` : 'Please login' }
</ div >
< div id = "status" >
Status: { status } | Synced: { isSynced ? 'Yes' : 'No' }
</ div >
< div id = "editor" ref = { editorElRef } />
< div className = "versions-section" >
< form onSubmit = { handleSaveVersion } >
< input
className = "version-input"
value = { versionInput }
onChange = { ( e ) => setVersionInput ( e . target . value ) }
placeholder = "Version name..."
/>
< button className = "save-version-btn" type = "submit" > Save Version </ button >
</ form >
< ul className = "version-list" >
{ versions . map (( v ) => (
< li key = { v . versionId } className = "version-item" >
< span className = "version-name" > { v . versionName } </ span >
< button className = "restore-btn" onClick = { () => handleRestoreVersion ( v . versionId ) } >
Restore
</ button >
</ li >
)) }
</ ul >
</ div >
</ div >
);
};
See all 100 lines
A complete collaborative TipTap editor with SDK initialization, user login, status display, and version management. HTML structure < div class = "app-container" >
< header class = "app-header" >
< h1 > TipTap CRDT Demo </ h1 >
< div id = "user-controls" ></ div >
</ header >
< main class = "app-content" >
< div id = "editor-header" > Please login to start editing </ div >
< div id = "status" class = "status-bar" > Connecting... </ div >
< div id = "editor" class = "editor-content" ></ div >
< div class = "versions-section" >
< h3 > Versions </ h3 >
< form id = "version-form" >
< input type = "text" class = "version-input" placeholder = "Version name..." />
< button type = "submit" class = "save-version-btn" > Save Version </ button >
</ form >
< ul class = "version-list" ></ ul >
</ div >
</ main >
</ div >
velt.ts import { initVelt } from '@veltdev/client' ;
import type { Velt } from '@veltdev/types' ;
let client : Velt | null = null ;
let veltInitialized = false ;
const veltInitSubscribers = new Map < string , ( velt : Velt ) => void >();
async function initializeVelt () {
client = await initVelt ( 'YOUR_API_KEY' );
client . setDocument ( 'my-document-id' , { documentName: 'My Document' });
client . getVeltInitState (). subscribe (( isReady ) => {
veltInitialized = !! isReady ;
if ( isReady && client ) {
veltInitSubscribers . forEach (( callback ) => callback ( client ));
}
});
}
export const subscribeToVeltInit = ( id : string , callback : ( velt : Velt ) => void ) => {
veltInitSubscribers . set ( id , callback );
if ( veltInitialized && client ) {
callback ( client );
}
};
export async function loginWithUser ( userId : string , name : string ) {
await client ?. identify ({ userId , name });
}
initializeVelt ();
See all 33 lines
editor.ts import { Editor } from '@tiptap/core' ;
import StarterKit from '@tiptap/starter-kit' ;
import { createCollaboration } from '@veltdev/tiptap-crdt' ;
import type { CollaborationManager } from '@veltdev/tiptap-crdt' ;
import type { Velt } from '@veltdev/types' ;
import { subscribeToVeltInit } from './velt' ;
let manager : CollaborationManager | null = null ;
let editor : Editor | null = null ;
async function initEditor ( veltClient : Velt ) {
// Create the collaboration manager
manager = await createCollaboration ({
editorId: 'my-document-id' ,
veltClient ,
initialContent: '<p>Hello TipTap CRDT!</p>' ,
onError : ( error ) => console . error ( 'Collaboration error:' , error ),
});
// Create the TipTap editor
editor = new Editor ({
element: document . querySelector ( '#editor' ) ! ,
extensions: [
StarterKit . configure ({ undoRedo: false }),
manager . createExtension (),
],
content: '' ,
});
// Wire up status display
manager . onStatusChange (( status ) => updateStatus ( status , manager ! . synced ));
manager . onSynced (( synced ) => updateStatus ( manager ! . status , synced ));
}
function updateStatus ( status : string , synced : boolean ) {
const el = document . getElementById ( 'status' );
if ( ! el ) return ;
if ( status === 'connected' ) {
el . textContent = synced ? 'Connected & Synced' : 'Connected, syncing...' ;
} else if ( status === 'connecting' ) {
el . textContent = 'Connecting...' ;
} else {
el . textContent = 'Disconnected' ;
}
}
// Wait for SDK to be ready
subscribeToVeltInit ( 'editor' , ( velt ) => initEditor ( velt ));
// Version management
export async function saveVersion ( name : string ) {
await manager ?. saveVersion ( name );
}
export async function getVersions () {
return await manager ?. getVersions () ?? [];
}
export async function restoreVersion ( versionId : string ) {
await manager ?. restoreVersion ( versionId );
}
See all 62 lines
How It Works
useCollaboration (React) / createCollaboration (non-React) creates a CollaborationManager. This initializes a CRDT Store (type: 'xml'), a Yjs Y.XmlFragment, and a SyncProvider.
The extension (returned by the hook or manager.createExtension()) bundles Collaboration (Yjs document binding) and CollaborationCaret (remote cursor rendering) as a single TipTap extension.
User types -> TipTap/ProseMirror transaction -> Y.XmlFragment mutation -> Yjs CRDT broadcasts via Velt backend -> all connected clients see the change.
Remote cursors are tracked via Yjs Awareness. The extension renders colored cursor labels at each remote user’s selection position.
Initial content is applied only once for brand-new documents. The manager waits for remote content before deciding the document is new.
Conflict resolution is handled by Yjs CRDTs, concurrent typing at different positions merges correctly, and formatting changes on different text ranges are independent.
Version management saves the full Yjs state as a named snapshot. Restoring a version replaces the current state and broadcasts to all clients.
Cleanup is automatic, the manager destroys the editor bindings, store, and all listeners when the editor is destroyed or the component unmounts.
APIs
React: useCollaboration()
The primary React hook for collaborative TipTap editing. Creates a CollaborationManager, initializes the CRDT store, and returns a TipTap Extension with cursor support.
Signature: useCollaboration(config: UseCollaborationConfig)
Params: UseCollaborationConfig
editorId: Unique identifier for this collaborative session.
initialContent: HTML content applied once for brand-new documents. Default: empty paragraph.
debounceMs: Throttle interval (ms) for backend writes. Default: 0.
forceResetInitialContent: If true, always clear and re-apply initialContent. Default: false.
veltClient: Explicit Velt client. Falls back to VeltProvider context.
onError: Error callback.
Returns: UseCollaborationReturn
extension: TipTap Extension bundling Yjs binding + cursor rendering. null while loading.
isLoading: true until CollaborationManager is initialized.
isSynced: true after the initial sync with the backend completes.
status: Connection status: 'connecting', 'connected', or 'disconnected'.
error: Most recent error, or null.
manager: The underlying CollaborationManager. null before initialization.
const { extension , isLoading , isSynced , status , error , manager } = useCollaboration ({
editorId: 'my-tiptap-editor' ,
initialContent: '<p>Start typing here...</p>' ,
onError : ( err ) => console . error ( 'Collaboration error:' , err ),
});
Non-React: createCollaboration()
Factory function that creates a CollaborationManager, calls initialize(), and returns a ready-to-use instance.
Signature: createCollaboration(config: CollaborationConfig): Promise<CollaborationManager>
Params: CollaborationConfig
editorId: Unique editor/document identifier for syncing.
veltClient: Velt client instance must have an authenticated user.
initialContent: HTML content applied once for brand-new documents.
debounceMs: Throttle interval (ms) for backend writes. Default: 0.
onError: Callback for non-fatal errors.
forceResetInitialContent: If true, always reset to initialContent. Default: false.
Returns: Promise<CollaborationManager>
import { createCollaboration } from '@veltdev/tiptap-crdt' ;
const manager = await createCollaboration ({
editorId: 'my-document-id' ,
veltClient: client ,
initialContent: '<p>Hello world</p>' ,
onError : ( error ) => console . error ( error ),
});
CollaborationManager Methods
Once the manager is available (non-null from the hook, or returned from createCollaboration), you can use its full API:
manager.createExtension()
Returns a single TipTap Extension that bundles Yjs document binding and remote cursor rendering. Non-React only, the React hook returns the extension directly.
const editor = new Editor ({
extensions: [
StarterKit . configure ({ undoRedo: false }),
manager . createExtension (),
],
});
manager.onStatusChange()
Subscribe to connection status changes.
Signature: manager.onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe
Returns: Unsubscribe (call to stop listening)
const unsubscribe = manager . onStatusChange (( status ) => {
console . log ( 'Connection:' , status );
});
unsubscribe (); // Stop listening
manager.onSynced()
Subscribe to sync state changes.
Signature: manager.onSynced(callback: (synced: boolean) => void): Unsubscribe
Returns: Unsubscribe
const unsubscribe = manager . onSynced (( synced ) => {
if ( synced ) console . log ( 'Initial sync complete' );
});
manager.initialized
Whether initialize() has completed.
manager.synced
Whether initial sync with the backend has completed.
manager.status
Current connection status.
Returns: SyncStatus ('connecting' | 'connected' | 'disconnected')
manager.saveVersion()
Save a named snapshot. Returns the version ID.
Signature: manager.saveVersion(name: string): Promise<string>
Returns: Promise<string>
const versionId = await manager . saveVersion ( 'Before major edit' );
manager.getVersions()
List all saved versions.
Returns: Promise<Version[]>
const versions = await manager . getVersions ();
manager.restoreVersion()
Restore to a saved version. Returns true on success. The restored state is pushed to all connected clients.
Signature: manager.restoreVersion(versionId: string): Promise<boolean>
Returns: Promise<boolean>
await manager . restoreVersion ( versions [ 0 ]. versionId );
manager.setStateFromVersion()
Apply a Version object’s state locally to the current document.
Signature: manager.setStateFromVersion(version: Version): Promise<void>
Returns: Promise<void>
await manager . setStateFromVersion ( version );
manager.getDoc()
Get the underlying Yjs document.
const doc = manager . getDoc ();
manager.getXmlFragment()
Get the XmlFragment bound to TipTap content.
Returns: Y.XmlFragment | null
const xml = manager . getXmlFragment ();
manager.getProvider()
Get the sync provider.
const provider = manager . getProvider ();
manager.getAwareness()
Get the Yjs Awareness instance.
const awareness = manager . getAwareness ();
manager.getStore()
Get the core CRDT Store.
const crdtStore = manager . getStore ();
manager.destroy()
Full cleanup (automatic on editor destroy or component unmount). Safe to call multiple times.
Custom Encryption
Encrypt CRDT data before it’s stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of type Uint8Array | number[].
React / Next.js
Other Frameworks
const encryptionProvider = {
encrypt : async ({ data }) => data . map (( n ) => n . toString ()). join ( '__' ),
decrypt : async ({ data }) => data . split ( '__' ). map (( s ) => parseInt ( s , 10 )),
};
< VeltProvider apiKey = "YOUR_API_KEY" encryptionProvider = { encryptionProvider } >
...
</ VeltProvider >
const encryptionProvider = {
encrypt : async ( config ) => {
return config . data . map (( num ) => num . toString ()). join ( '__' );
},
decrypt : async ( config ) => {
return config . data . split ( '__' ). map (( str ) => parseInt ( str , 10 ));
},
};
client . setEncryptionProvider ( encryptionProvider );
See also: setEncryptionProvider()
· VeltEncryptionProvider
· EncryptConfig
· DecryptConfig
Migration Guide: v1 to v2
React
Overview
The v2 API replaces useVeltTiptapCrdtExtension() with useCollaboration(). The new hook provides richer reactive state (status, sync, error) and exposes the CollaborationManager for advanced use.
Key Changes
Aspect v1 (deprecated) v2 (current) Entry point useVeltTiptapCrdtExtension(config)useCollaboration(config)Extension access response.VeltCrdtresponse.extensionStore access response.store (VeltTipTapStore)response.manager (CollaborationManager)Version management store.saveVersion(), store.getVersions()manager.saveVersion(), manager.getVersions()Status tracking Not available response.status, response.isSyncedError handling onConnectionError callbackonError callback + response.error stateSync notification onSynced callback (fires once)response.isSynced (reactive state)Cleanup Automatic on unmount Automatic on unmount
Step-by-Step
1. Replace the hook:
// Before (v1)
import { useVeltTiptapCrdtExtension } from '@veltdev/tiptap-crdt-react' ;
const { VeltCrdt , isLoading , store } = useVeltTiptapCrdtExtension ({
editorId: 'my-doc' ,
initialContent: '<p>Hello</p>' ,
onSynced : ( doc ) => console . log ( 'Synced!' ),
onConnectionError : ( err ) => console . error ( err ),
});
// After (v2)
import { useCollaboration } from '@veltdev/tiptap-crdt-react' ;
const { extension , isLoading , isSynced , status , error , manager } = useCollaboration ({
editorId: 'my-doc' ,
initialContent: '<p>Hello</p>' ,
onError : ( err ) => console . error ( err ),
});
2. Replace extension usage:
// Before (v1)
const editor = useEditor ({
extensions: [ StarterKit . configure ({ undoRedo: false }), ... ( VeltCrdt ? [ VeltCrdt ] : [])],
}, [ VeltCrdt ]);
// After (v2)
useEffect (() => {
if ( ! extension || ! editorElRef . current ) return ;
const editor = new Editor ({
element: editorElRef . current ,
extensions: [ StarterKit . configure ({ undoRedo: false }), extension ],
});
return () => editor . destroy ();
}, [ extension ]);
3. Replace version management:
// Before (v1): store.getVersions(), store.setStateFromVersion(version)
// After (v2): manager.getVersions(), manager.restoreVersion(versionId)
4. Add status monitoring (new in v2):
if ( error ) return < div > Error: { error . message } </ div > ;
if ( isLoading ) return < div > Connecting... </ div > ;
< div > Status: { status } | Synced: { isSynced ? 'Yes' : 'No' } </ div >
Non-React
Overview
The v2 API replaces the callback-based createVeltTiptapCrdtExtension() with an async createCollaboration() factory that returns a CollaborationManager.
Key Changes
Aspect v1 (deprecated) v2 (current) Entry point createVeltTiptapCrdtExtension(config, callback)await createCollaboration(config)Return value Cleanup function CollaborationManager instanceExtension access Via callback: response.VeltCrdt Via method: manager.createExtension() Store access Via callback: response.store Via method: manager.getStore() Version management store.saveVersion(), store.getVersions()manager.saveVersion(), manager.getVersions()Status tracking Not available manager.onStatusChange(), manager.onSynced()Cleanup Call returned cleanup function manager.destroy() (or automatic via editor)Error handling onConnectionError callbackonError callbackSync notification onSynced callback (fires once)manager.onSynced() (subscribable)
Step-by-Step
1. Replace the extension creation:
// Before (v1)
import { createVeltTiptapCrdtExtension } from '@veltdev/tiptap-crdt' ;
let cleanup ;
let editor ;
cleanup = createVeltTiptapCrdtExtension (
{
editorId: 'my-doc' ,
veltClient: client ,
initialContent: '<p>Hello</p>' ,
onSynced : ( doc ) => console . log ( 'Synced!' ),
onConnectionError : ( err ) => console . error ( err ),
},
({ VeltCrdt , store }) => {
editor = new Editor ({
extensions: [
StarterKit . configure ({ undoRedo: false }),
... ( VeltCrdt ? [ VeltCrdt ] : []),
],
});
}
);
// After (v2)
import { createCollaboration } from '@veltdev/tiptap-crdt' ;
const manager = await createCollaboration ({
editorId: 'my-doc' ,
veltClient: client ,
initialContent: '<p>Hello</p>' ,
onError : ( err ) => console . error ( err ),
});
const editor = new Editor ({
extensions: [
StarterKit . configure ({ undoRedo: false }),
manager . createExtension (),
],
});
// Subscribe to sync state (replaces onSynced callback)
manager . onSynced (( synced ) => {
if ( synced ) console . log ( 'Synced!' );
});
2. Replace version management:
// Before (v1): store.getVersions(), store.setStateFromVersion(version)
// After (v2): manager.getVersions(), manager.restoreVersion(versionId)
3. Replace cleanup:
// Before (v1): cleanup()
// After (v2): manager.destroy() or editor.destroy() (triggers automatic cleanup)
4. Add status monitoring (new in v2):
manager . onStatusChange (( status ) => console . log ( 'Status:' , status ));
manager . onSynced (( synced ) => console . log ( 'Synced:' , synced ));
5. Access Yjs internals:
// Before (v1): store.getYDoc(), store.getYXml()
// After (v2): manager.getDoc(), manager.getXmlFragment(), manager.getAwareness(), manager.getProvider()
Legacy API (v1)
The v1 API is deprecated. Use the v2 useCollaboration hook (React) or createCollaboration (non-React) for all new integrations. The v1 API is still exported for backward compatibility.
React: useVeltTiptapCrdtExtension() (deprecated)
A React hook that returns a TipTap extension and store. Internally delegates to useCollaboration (v2) via a compatibility wrapper.
import { useVeltTiptapCrdtExtension } from '@veltdev/tiptap-crdt-react' ;
const { VeltCrdt , isLoading , store } = useVeltTiptapCrdtExtension ({
editorId: 'my-document-id' ,
initialContent: '<p>Hello world</p>' ,
onSynced : ( doc ) => console . log ( 'Synced!' ),
onConnectionError : ( err ) => console . error ( err ),
});
const editor = useEditor ({
extensions: [
StarterKit . configure ({ undoRedo: false }),
... ( VeltCrdt ? [ VeltCrdt ] : []),
],
content: '' ,
}, [ VeltCrdt ]);
React: VeltTiptapCrdtExtensionConfig (deprecated)
Property Type Required Description editorIdstringYes Unique editor identifier. initialContentstringNo HTML content for new documents. debounceMsnumberNo Throttle interval (ms). onSynced(doc?: Y.Doc) => voidNo Called once when initial sync completes. onConnectionError(error: Error) => voidNo Called on connection errors. veltClientVeltNo Velt client instance.
React: VeltTiptapCrdtExtensionResponse (deprecated)
Property Type Description VeltCrdtExtension | nullTipTap extension to add to the editor. isLoadingbooleantrue until the extension is ready.storeVeltTipTapStore | nullStore instance for version management.
Non-React: createVeltTiptapCrdtExtension() (deprecated)
Creates a collaboration extension using a callback-based pattern. The function subscribes to the Velt SDK lifecycle and invokes the onReady callback when the extension and store are ready.
Signature: createVeltTiptapCrdtExtension(config: VeltTipTapStoreConfig, onReady: (response: VeltTiptapCrdtExtensionResponse) => void): () => void
Returns: () => void (cleanup function)
import { createVeltTiptapCrdtExtension } from '@veltdev/tiptap-crdt' ;
const cleanup = createVeltTiptapCrdtExtension (
{
editorId: 'my-document-id' ,
veltClient: client ,
initialContent: '<p>Hello world</p>' ,
},
({ VeltCrdt , store }) => {
const editor = new Editor ({
element: document . querySelector ( '#editor' ),
extensions: [
StarterKit . configure ({ undoRedo: false }),
... ( VeltCrdt ? [ VeltCrdt ] : []),
],
content: '' ,
});
}
);
// Call cleanup() to tear down
cleanup ();
Non-React: VeltTipTapStoreConfig (deprecated)
Property Type Required Description editorIdstringYes Unique editor identifier. initialContentstringNo HTML content for new documents. debounceMsnumberNo Throttle interval (ms). onSynced(doc?: Y.Doc) => voidNo Called once when initial sync completes. onConnectionError(error: Error) => voidNo Called on connection errors. veltClientVeltNo Velt client instance.
Non-React: VeltTiptapCrdtExtensionResponse (deprecated)
Property Type Description VeltCrdtExtension | nullTipTap extension to add to the editor. storeVeltTipTapStore | nullStore instance for version management. errorstring | nullError message if initialization failed.
VeltTipTapStore Methods (deprecated)
Method Returns Description getStore()Store<string>Get the core CRDT Store. getYXml()Y.XmlFragment | nullGet the Yjs XmlFragment. getYDoc()Y.DocGet the Yjs Doc. getCollabExtension()ExtensionGet the TipTap collaboration extension. isConnected()booleanCheck if connected to backend. setStateFromVersion(v)Promise<void>Apply a version’s state. destroy()voidClean up all resources.