@veltdev/codemirror-crdt is a library for enabling real-time, collaborative editing in CodeMirror using CRDT (Conflict-free Replicated Data Types). It integrates Yjs for conflict resolution, awareness (e.g., user cursors), and undo/redo, powered by the Velt collaboration platform. This allows multiple users to edit code simultaneously without conflicts, with features like presence indicators and synced changes.

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/codemirror-crdt @veltdev/react

Step 2: Setup Velt

Wrap your app with the VeltProvider to enable authentication and collaboration features. See Velt Setup Docs for details.
import { VeltProvider } from '@veltdev/react';

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

Step 3: Import Required Components

Import the CodeMirror and Velt CRDT utilities you’ll need:
import { createVeltCodeMirrorStore } from '@veltdev/codemirror-crdt';
import { yCollab } from 'y-codemirror.next';
import { EditorState } from '@codemirror/state';
import { basicSetup, EditorView } from 'codemirror';
import { useVeltClient } from '@veltdev/react';

Step 4: Setup Component State

Use component state to mount the editor only after the CRDT store is ready and re-initialize on user/store changes to avoid early init bugs and duplicate providers.
const CollaborativeCodeEditor: React.FC = () => {
  // DOM ref where the CodeMirror editor will mount
  const editorRef = useRef<HTMLDivElement | null>(null);

  // Track the EditorView instance for cleanup
  const editorViewRef = useRef<EditorView | null>(null);

  // Store readiness flag
  const [storeReady, setStoreReady] = useState(false);

  // Velt client for auth and connectivity
  const { client } = useVeltClient();

  // Hold the store instance
  const storeRef = useRef<Awaited<ReturnType<typeof createVeltCodeMirrorStore>> | null>(null);

Step 5: Initialize the VeltCodeMirror Store

Initialize one store per editor. For multiple editors on the same page, create multiple stores and give each a unique editorId so their providers, content, and cursors stay isolated, enabling multiple editor support.
  useEffect(() => {
    if (!client) return;

    let mounted = true;

    const init = async () => {
      const store = await createVeltCodeMirrorStore({
        editorId: 'my-codemirror-collab-editor', // Unique identifier (e.g., 'index.html')
        veltClient: client!
      });
      if (!mounted) return;
      storeRef.current = store;
      setStoreReady(true);
    };

    init();

    return () => {
      mounted = false;
      // Destroy CodeMirror view
      if (editorViewRef.current) {
        editorViewRef.current.destroy();
        editorViewRef.current = null;
      }
      // Destroy store
      if (storeRef.current) {
        storeRef.current.destroy();
        storeRef.current = null;
      }
    };
  }, [client]);

Step 6: Configure CodeMirror Collaboration

Create an EditorState that uses the Yjs text, awareness, and undo manager from the store. Mount an EditorView when ready.
  useEffect(() => {
    if (!storeReady || !editorRef.current || !storeRef.current) return;

    const store = storeRef.current;

    const state = EditorState.create({
      doc: store.getYText()?.toString() ?? '',
      extensions: [
        basicSetup,
        // Add language and tooling extensions as needed (e.g., javascript(), css(), html(), autocompletion())
        yCollab(store.getYText(), store.getAwareness(), { undoManager: store.getUndoManager() }),
      ],
    });

    const view = new EditorView({
      state,
      parent: editorRef.current,
    });

    editorViewRef.current = view;
  }, [storeReady]);

Step 7: Render the Editor

Render the editor once it’s initialized. Show a loading state while connecting and display the live connection status next to the editor.
  return (
    <div className="editor-container">
      <div className="editor-header">Collaborative Code Editor</div>
      <div className="editor-content" ref={editorRef} />
      <div className="status">{storeReady ? 'Connected to collaborative session' : 'Connecting to collaborative session...'}</div>
    </div>
  );
};

export default CollaborativeCodeEditor;

Notes

  • Unique editorId: Use a unique editorId per editor instance.
  • Wait for auth: Initialize the store only after the Velt client is ready.
  • Use yCollab: Pass the store’s Yjs text, awareness, and undo manager to yCollab.
  • Initialize and clean up: Destroy the EditorView and store on unmount to avoid leaks.

Testing and Debugging

To test collaboration:
  1. Open two browser windows with different user sessions.
  2. Type 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.
  • Editor not loading: Confirm the Velt client is initialized and the API key is valid.
  • Disconnected session: Check network and that the provider is active via the store.

Complete Example

Expandable Example
import React, { useEffect, useRef, useState } from 'react';
import { createVeltCodeMirrorStore } from '@veltdev/codemirror-crdt';
import { yCollab } from 'y-codemirror.next';
import { EditorState } from '@codemirror/state';
import { basicSetup, EditorView } from 'codemirror';
import { useVeltClient } from '@veltdev/react';

const CollaborativeCodeEditor: React.FC = () => {
  const editorRef = useRef<HTMLDivElement | null>(null);
  const editorViewRef = useRef<EditorView | null>(null);
  const [storeReady, setStoreReady] = useState(false);
  const { client } = useVeltClient();
  const storeRef = useRef<Awaited<ReturnType<typeof createVeltCodeMirrorStore>> | null>(null);

  useEffect(() => {
    if (!client) return;

    let mounted = true;

    const init = async () => {
      const store = await createVeltCodeMirrorStore({
        editorId: 'velt-codemirror-crdt-demo',
        veltClient: client!,
      });
      if (!mounted) return;
      storeRef.current = store;
      setStoreReady(true);
    };

    init();

    return () => {
      mounted = false;
      if (editorViewRef.current) {
        editorViewRef.current.destroy();
        editorViewRef.current = null;
      }
      if (storeRef.current) {
        storeRef.current.destroy();
        storeRef.current = null;
      }
    };
  }, [client]);

  useEffect(() => {
    if (!storeReady || !editorRef.current || !storeRef.current) return;

    const store = storeRef.current;

    const state = EditorState.create({
      doc: store.getYText()?.toString() ?? '',
      extensions: [
        basicSetup,
        yCollab(store.getYText(), store.getAwareness(), { undoManager: store.getUndoManager() }),
      ],
    });

    const view = new EditorView({ state, parent: editorRef.current });
    editorViewRef.current = view;
  }, [storeReady]);

  return (
    <div className="editor-container">
      <div className="editor-header">Collaborative Code Editor</div>
      <div className="editor-content" ref={editorRef} />
      <div className="status">{storeReady ? 'Connected to collaborative session' : 'Connecting to collaborative session...'}</div>
    </div>
  );
};

export default CollaborativeCodeEditor;

Encryption

You can 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<string>): Promise<string> {
  const encryptedData = await yourEncryptDataMethod(config.data);
  return encryptedData;
}

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

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

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

APIs

createVeltCodeMirrorStore

Create and initialize a collaborative CodeMirror store instance.
const store = await createVeltCodeMirrorStore({ editorId: 'velt-cm-doc', veltClient: client });

getStore()

Get the underlying Store<string> that manages document state and synchronization.
  • Returns: Store<string>
const underlying = store.getStore();

getYDoc()

Access the Yjs document used for collaborative editing.
  • Returns: Y.Doc
const ydoc = store.getYDoc();

getYText()

Get the shared Y.Text bound to the current CodeMirror document.
  • Returns: Y.Text | null
const docText = store.getYText()?.toString() ?? '';

getAwareness()

Access the Yjs Awareness instance associated with the store.
  • Returns: Awareness
yCollab(store.getYText()!, store.getAwareness(), { undoManager: store.getUndoManager() });

getUndoManager()

Access the Yjs UndoManager for collaborative undo/redo.
  • Returns: Y.UndoManager
const extensions = [
  basicSetup,
  yCollab(store.getYText()!, store.getAwareness(), { undoManager: store.getUndoManager() }),
];

destroy()

Clean up listeners and release resources.
  • Returns: void
store.destroy();