Skip to main content
The @veltdev/blocknote-crdt-react and @veltdev/blocknote-crdt libraries enable real-time collaborative editing on BlockNote 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:
npm install @veltdev/blocknote-crdt-react @veltdev/blocknote-crdt @veltdev/react @veltdev/types @blocknote/core @blocknote/react @blocknote/mantine yjs

Step 2: Setup Velt

Initialize the Velt client and set the document context. This is required for the collaboration engine to work.
Wrap your app with VeltProvider and use useSetDocument to scope the collaborative session. Users must be identified before collaboration starts — cursor names and colors are derived from user identity.
import { VeltProvider, useSetDocument, useVeltClient } from '@veltdev/react';

function App() {
  return (
    <VeltProvider apiKey="YOUR_API_KEY">
      <YourApp />
    </VeltProvider>
  );
}

function YourApp() {
  useSetDocument('my-blocknote-document', { documentName: 'My Document' });
  return <BlockNoteEditorComponent />;
}
Identify the current user so the collaboration engine can track cursors and authorship:
const { client } = useVeltClient();

await client.identify({
  userId: 'user-1',
  name: 'Alice',
  email: 'alice@example.com',
  organizationId: 'my-org',
});

Step 3: Initialize Collaborative Editor

  • Initialize the collaboration manager and use it to create the BlockNote 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.
Use the useCollaboration hook to manage the entire CRDT lifecycle. It creates a CollaborationManager, initializes the CRDT store, and returns a BlockNoteCollaborationConfig. You pass this config to useCreateBlockNote({ collaboration: ... }) when creating your BlockNote editor.
import { useCollaboration } from '@veltdev/blocknote-crdt-react';
import { useCreateBlockNote } from '@blocknote/react';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';

function BlockNoteEditorComponent() {
  const {
    collaborationConfig,
    isLoading,
    isSynced,
    status,
    error,
    manager,
    saveVersion,
    getVersions,
    restoreVersion,
  } = useCollaboration({
    editorId: 'my-blocknote-editor',
    onError: (err) => console.error('Collaboration error:', err),
  });

  const editor = useCreateBlockNote(
    collaborationConfig
      ? { collaboration: collaborationConfig }
      : {},
    [collaborationConfig],
  );

  if (error) return <div>Error: {error.message}</div>;
  if (isLoading || !collaborationConfig) return <div>Connecting...</div>;

  return <BlockNoteView editor={editor} theme="light" />;
}
When the collaboration config is provided, BlockNote automatically uses the Y.XmlFragment as the document source, enables remote cursor rendering, and switches undo/redo to Yjs UndoManager. No additional configuration is needed.

Step 4: Status Monitoring (Optional)

Monitor the connection status and sync state to display UI indicators.
The useCollaboration hook returns reactive status, isSynced, and error state:
const { collaborationConfig, isLoading, isSynced, status, error } = useCollaboration({
  editorId: 'my-blocknote-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>

Step 5: Version Management (Optional)

Save and restore named snapshots of the document state.
Version methods are returned directly from useCollaboration as first-class APIs:
// Save a version
const versionId = await saveVersion('Before major edit');

// List all versions
const versions = await getVersions();

// Restore a specific version
await restoreVersion(versions[0].versionId);

Step 6: 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.
forceResetInitialContent wipes all existing content every time the page loads with this flag. Do not use it in production user flows — it is intended for development and admin use only.
const collab = useCollaboration({
  editorId: 'my-blocknote-editor',
  initialContent: [{ type: 'paragraph', content: 'Fresh start!' }],
  forceResetInitialContent: true,
});

Step 7: CRDT Event Subscription (Optional)

Listen for real-time sync events using getCrdtElement().on() from the Velt client.
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]);

Step 8: 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[].
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>
See also: setEncryptionProvider() · VeltEncryptionProvider · EncryptConfig · DecryptConfig

Step 9: Error Handling (Optional)

Pass an onError callback to handle initialization or runtime errors. The error reactive state is also available for rendering.
const collab = useCollaboration({
  editorId: 'my-blocknote-editor',
  onError: (error) => console.error('Collaboration error:', error),
});

if (collab.error) {
  return <div>Error: {collab.error.message}</div>;
}

Step 10: Access Yjs Internals (Advanced)

The manager exposes escape hatches for direct Yjs manipulation when needed.
// 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();

Step 11: Cleanup

Cleanup is handled by calling manager.destroy(), which cascades to the store, provider, and all listeners. Safe to call multiple times.
// Automatic on component unmount via useEffect cleanup
// Or manually:
manager.destroy();

Notes

  • Unique editorId: Use a unique editorId per editor instance.
  • Pass collaborationConfig: Provide the collaboration config from useCollaboration (React) or manager.getCollaborationConfig() (non-React) to the BlockNote editor.
  • Undo/redo: Automatically switches to Yjs UndoManager when collaboration is enabled.

Testing and Debugging

To test collaboration:
  1. Login two unique users on two browser profiles and open the same page in both.
  2. 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.
Use the VeltCrdtStoreMap debugging interface to inspect and monitor your CRDT stores in real-time from the browser console.

Complete Example

A complete collaborative BlockNote editor with user login, status display, and version management.App.tsx
Complete 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') || 'blocknote-crdt-react-demo-doc-1',
    documentName: params.get('documentName') || 'BlockNote 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>
);
Editor.tsx
Complete Editor.tsx
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useVeltClient, useCurrentUser } from '@veltdev/react';
import { useCollaboration } from '@veltdev/blocknote-crdt-react';
import { useCreateBlockNote } from '@blocknote/react';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import type { Version } from '@veltdev/crdt';

function getEditorParams() {
  const params = new URLSearchParams(window.location.search);
  return {
    documentId: params.get('documentId') || 'blocknote-crdt-react-demo-doc-1',
    forceReset: params.get('forceResetInitialContent') === 'true',
  };
}

export const EditorComponent = () => {
  const veltUser = useCurrentUser();
  const [versions, setVersions] = useState<Version[]>([]);
  const [versionInput, setVersionInput] = useState('');

  const params = useMemo(() => getEditorParams(), []);

  const {
    collaborationConfig,
    isLoading,
    isSynced,
    status,
    manager,
    saveVersion,
    getVersions,
    restoreVersion,
  } = useCollaboration({
    editorId: params.documentId,
    forceResetInitialContent: params.forceReset,
    onError: (err) => console.error('Collaboration error:', err),
  });

  const editor = useCreateBlockNote(
    collaborationConfig
      ? {
          collaboration: collaborationConfig,
          placeholders: { default: '', emptyDocument: '' },
        }
      : {},
    [collaborationConfig],
  );

  const refreshVersions = useCallback(async () => {
    const v = await getVersions();
    setVersions(v);
  }, [getVersions]);

  useEffect(() => {
    if (manager) refreshVersions();
  }, [manager, refreshVersions]);

  const handleSaveVersion = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    if (!versionInput.trim()) return;
    await saveVersion(versionInput.trim());
    setVersionInput('');
    await refreshVersions();
  }, [versionInput, saveVersion, refreshVersions]);

  const handleRestoreVersion = useCallback(async (versionId: string) => {
    await restoreVersion(versionId);
    await refreshVersions();
  }, [restoreVersion, refreshVersions]);

  const getStatusText = () => {
    if (isLoading) return 'Connecting...';
    switch (status) {
      case 'connected': return isSynced ? 'Connected & Synced' : 'Connected, syncing...';
      case 'connecting': return 'Connecting...';
      case 'disconnected': return 'Disconnected';
      default: return status;
    }
  };

  return (
    <div className="editor-container">
      <div id="editor-header">
        {veltUser ? `Editing as ${veltUser.name}` : 'Please login'}
      </div>
      <div id="status">{getStatusText()}</div>
      <div id="editor" className="editor-content">
        {editor && collaborationConfig ? (
          <BlockNoteView editor={editor} theme="light" />
        ) : (
          <div className="editor-loading">Loading editor...</div>
        )}
      </div>
      <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>
  );
};

How It Works

  1. useCollaboration (React) / createCollaboration (non-React) creates a CollaborationManager. This initializes a CRDT Store (type: 'xml', content key 'document-store'), a Yjs Y.XmlFragment, and a SyncProvider.
  2. The collaboration config (returned by the hook or manager.getCollaborationConfig()) contains the SyncProvider, Y.XmlFragment, user info, and cursor label settings. This config is passed to useCreateBlockNote({ collaboration: ... }) (React) or BlockNoteEditor.create({ collaboration: ... }) (non-React).
  3. User types -> BlockNote/ProseMirror transaction -> Y.XmlFragment mutation -> Yjs CRDT broadcasts via Velt backend -> all connected clients see the change.
  4. Remote cursors are tracked via Yjs Awareness. BlockNote renders colored cursor labels at each remote user’s selection position using its built-in collaboration cursor support.
  5. Undo/redo automatically switches to Yjs UndoManager when collaboration is enabled — no need to disable any history extension.
  6. Initial content is applied only once for brand-new documents. The manager waits for remote content before deciding the document is new.
  7. Conflict resolution is handled by Yjs CRDTs — concurrent typing at different positions merges correctly, and formatting changes on different text ranges are independent.
  8. Version management saves the full Yjs state as a named snapshot. Restoring a version replaces the current state and broadcasts to all clients.
  9. Cleanup is automatic — the manager destroys the store, provider, and all listeners when destroyed or the component unmounts.

APIs

React: useCollaboration()

The primary React hook for collaborative BlockNote editing. Creates a CollaborationManager, initializes the CRDT store, and returns a BlockNoteCollaborationConfig with cursor support.
  • Signature: useCollaboration(config: UseCollaborationConfig)
  • Params: UseCollaborationConfig
    • editorId: Unique identifier for this collaborative session.
    • initialContent: Block content applied once for brand-new documents.
    • 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.
    • showCursorLabels: Cursor label display mode. Default: 'activity'.
    • onError: Error callback.
  • Returns: UseCollaborationReturn
    • collaborationConfig: BlockNote collaboration config. 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.
    • saveVersion: Save a named version snapshot. Returns the version ID, or empty string on failure.
    • getVersions: List all saved versions. Returns empty array on failure.
    • restoreVersion: Restore to a saved version. Returns true on success, false on failure.
const {
  collaborationConfig,
  isLoading,
  isSynced,
  status,
  error,
  manager,
  saveVersion,
  getVersions,
  restoreVersion,
} = useCollaboration({
  editorId: 'my-blocknote-editor',
  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: Block 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/blocknote-crdt';

const manager = await createCollaboration({
  editorId: 'my-document-id',
  veltClient: client,
  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.getCollaborationConfig()

Returns the collaboration config object that BlockNote expects. Pass this to BlockNoteEditor.create({ collaboration: ... }) or useCreateBlockNote({ collaboration: ... }).
  • Params: options?: { showCursorLabels?: 'activity' | 'always' } — Optional cursor display overrides
  • Returns: BlockNoteCollaborationConfig | null
const collabConfig = manager.getCollaborationConfig();

const editor = BlockNoteEditor.create({
  collaboration: collabConfig,
});

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.
  • Returns: boolean

manager.synced

Whether initial sync with the backend has completed.
  • Returns: boolean

manager.status

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

manager.saveVersion()

Save a named snapshot of the current document state.
  • Signature: manager.saveVersion(name: string): Promise<string>
  • Returns: Promise<string> (version ID, or empty string on failure)
const versionId = await manager.saveVersion('Before major edit');

manager.getVersions()

List all saved versions for this document.
  • Returns: Promise<Version[]> (empty array on failure)
const versions = await manager.getVersions();

manager.restoreVersion()

Restore the document to a previously saved version. The restored state is pushed to all connected clients.
  • Signature: manager.restoreVersion(versionId: string): Promise<boolean>
  • Returns: Promise<boolean> (true on success, false on failure)
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.
  • Returns: Y.Doc | null
const doc = manager.getDoc();

manager.getXmlFragment()

Get the XmlFragment bound to BlockNote’s document-store key.
  • Returns: Y.XmlFragment | null
const xml = manager.getXmlFragment();

manager.getProvider()

Get the sync provider.
  • Returns: SyncProvider | null
const provider = manager.getProvider();

manager.getAwareness()

Get the Yjs Awareness instance.
  • Returns: Awareness | null
const awareness = manager.getAwareness();

manager.getStore()

Get the core CRDT Store.
  • Returns: Store<string> | null
const crdtStore = manager.getStore();

manager.destroy()

Full cleanup (automatic on editor destroy or component unmount). Safe to call multiple times.
  • Returns: void
manager.destroy();

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[].
async function encryptData(config: EncryptConfig<number[]>): Promise<string> {
  const encryptedData = await yourEncryptDataMethod(config.data);
  return encryptedData;
}

async function decryptData(config: DecryptConfig<string>): Promise<number[]> {
  const decryptedData = await yourDecryptDataMethod(config.data);
  return decryptedData;
}

const encryptionProvider: VeltEncryptionProvider<number[], string> = {
  encrypt: encryptData,
  decrypt: decryptData,
};

<VeltProvider
  apiKey="YOUR_API_KEY"
  encryptionProvider={encryptionProvider}
/>
See also: setEncryptionProvider() · VeltEncryptionProvider · EncryptConfig · DecryptConfig

Migration Guide: v1 to v2

React

Overview

The v2 API replaces useVeltBlockNoteCrdtExtension() with useCollaboration(). The new hook provides richer reactive state (status, sync, error), returns version management methods directly, and exposes the CollaborationManager for advanced use.

Key Changes

Aspectv1 (deprecated)v2 (current)
Entry pointuseVeltBlockNoteCrdtExtension(config)useCollaboration(config)
Collab configresponse.collaborationConfigresponse.collaborationConfig (same usage)
Store accessresponse.store (VeltBlockNoteStore)response.manager (CollaborationManager)
Version managementVia store methodssaveVersion, getVersions, restoreVersion (first-class)
Status trackingNot availableresponse.status, response.isSynced
Error handlingNot availableonError callback + response.error state
Cursor labelsNot configurableshowCursorLabels: 'activity' | 'always'
CleanupAutomatic on unmountAutomatic on unmount

Step-by-Step

1. Replace the hook:
// Before (v1)
import { useVeltBlockNoteCrdtExtension } from '@veltdev/blocknote-crdt-react';
const { collaborationConfig, isLoading, store } = useVeltBlockNoteCrdtExtension({
  editorId: 'my-doc',
});

// After (v2)
import { useCollaboration } from '@veltdev/blocknote-crdt-react';
const { collaborationConfig, isLoading, isSynced, status, error, manager, saveVersion, getVersions, restoreVersion } = useCollaboration({
  editorId: 'my-doc',
  onError: (err) => console.error(err),
});
2. Editor creation (same pattern):
const editor = useCreateBlockNote(
  collaborationConfig ? { collaboration: collaborationConfig } : {},
  [collaborationConfig],
);
3. Use version management (new in v2):
await saveVersion('Draft v1');
const versions = await getVersions();
await restoreVersion(versions[0].versionId);
4. Add status monitoring (new in v2):
if (error) return <div>Error: {error.message}</div>;
<div>Status: {status} | Synced: {isSynced ? 'Yes' : 'No'}</div>

Non-React

Overview

The non-React v2 API provides createCollaboration() from @veltdev/blocknote-crdt, replacing any previous callback-based patterns. The returned CollaborationManager provides getCollaborationConfig() for BlockNote editor creation.

Step-by-Step

1. Create the manager:
import { createCollaboration } from '@veltdev/blocknote-crdt';
const manager = await createCollaboration({ editorId: 'my-doc', veltClient: client });
2. Create the editor:
const collabConfig = manager.getCollaborationConfig();
const editor = BlockNoteEditor.create({ collaboration: collabConfig });
editor.mount(document.getElementById('editor'));
3. Monitor status:
manager.onStatusChange((status) => console.log('Status:', status));
manager.onSynced((synced) => console.log('Synced:', synced));
4. Version management:
await manager.saveVersion('Draft v1');
const versions = await manager.getVersions();
await manager.restoreVersion(versionId);
5. Cleanup:
manager.destroy();

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: useVeltBlockNoteCrdtExtension() (deprecated)

A React hook that returns a collaboration config and store for BlockNote integration. Internally delegates to useCollaboration (v2) via a compatibility wrapper.
import { useVeltBlockNoteCrdtExtension } from '@veltdev/blocknote-crdt-react';

const { collaborationConfig, isLoading } = useVeltBlockNoteCrdtExtension({
  editorId: 'my-document-id',
  initialContent: JSON.stringify([{ type: 'paragraph', content: '' }]),
});

const editor = useCreateBlockNote({
  collaboration: collaborationConfig,
}, [collaborationConfig]);

return (
  <BlockNoteView
    editor={editor}
    key={collaborationConfig ? 'collab-on' : 'collab-off'}
  />
);

React: VeltBlockNoteCrdtExtensionConfig (deprecated)

PropertyTypeRequiredDescription
editorIdstringYesUnique editor identifier.
initialContentstringNoJSON string of blocks for new documents.
debounceMsnumberNoThrottle interval (ms).

React: VeltBlockNoteCrdtExtensionResponse (deprecated)

PropertyTypeDescription
collaborationConfig{ fragment: any; provider: any; user: { name: string; color: string } } | nullConfig to pass to useCreateBlockNote.
storeVeltBlockNoteStore | nullStore instance for version management.
isLoadingbooleantrue until the store is ready.

VeltBlockNoteStore Methods (deprecated)

MethodReturnsDescription
getStore()Store<string>Get the core CRDT Store.
getYXml()Y.XmlFragment | nullGet the Yjs XmlFragment.
getYDoc()Y.DocGet the Yjs Doc.
isConnected()booleanCheck if connected to backend.
setStateFromVersion(v)Promise<void>Apply a version’s state.
destroy()voidClean up all resources.