Skip to main content
ProseKit supports real-time collaboration through integration with Yjs and Loro, two powerful CRDT (Conflict-free Replicated Data Type) libraries. This guide will show you how to set up collaborative editing in your application.

Collaboration Options

ProseKit supports two collaboration backends:
LibraryDescriptionBest For
YjsMature CRDT library with extensive ecosystemProduction applications, WebRTC, WebSocket
LoroModern CRDT library with time-travel and efficient syncNew projects, advanced features

Collaboration with Yjs

Yjs is a mature CRDT implementation with a rich ecosystem of providers for different network transports.
1

Install dependencies

npm install yjs y-prosemirror y-websocket
2

Create the Yjs document

import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

// Create a Yjs document
const ydoc = new Y.Doc()

// Create a WebSocket provider for syncing
const provider = new WebsocketProvider(
  'ws://localhost:1234', // Your WebSocket server URL
  'my-document-id',      // Unique document identifier
  ydoc
)

// Create awareness for cursor positions
const awareness = provider.awareness
3

Create editor with Yjs extension

import { defineBasicExtension } from 'prosekit/basic'
import { createEditor, union } from 'prosekit/core'
import { defineYjs } from 'prosekit/extensions/yjs'

const extension = union(
  defineBasicExtension(),
  defineYjs({
    doc: ydoc,
    awareness: awareness,
  }),
)

const editor = createEditor({ extension })
4

Display collaborator cursors

The Yjs extension automatically handles cursor synchronization. You can customize cursor appearance with CSS:
/* Collaborator cursor */
.ProseMirror-yjs-cursor {
  position: absolute;
  border-left: 2px solid;
  margin-left: -1px;
  pointer-events: none;
}

/* Collaborator selection */
.ProseMirror-yjs-selection {
  background-color: rgba(59, 130, 246, 0.2);
}

/* Cursor label with user name */
.ProseMirror-yjs-cursor-label {
  position: absolute;
  top: -1.4em;
  left: -1px;
  font-size: 0.75rem;
  padding: 0.125rem 0.25rem;
  border-radius: 0.25rem;
  color: white;
  white-space: nowrap;
}
5

Set user information

// Set local user information
awareness.setLocalStateField('user', {
  name: 'John Doe',
  color: '#3b82f6',
})

Complete Yjs Example

import { defineBasicExtension } from 'prosekit/basic'
import { createEditor, union } from 'prosekit/core'
import { defineYjs } from 'prosekit/extensions/yjs'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

function createCollaborativeEditor(documentId: string, userId: string) {
  // Create Yjs document
  const ydoc = new Y.Doc()

  // Connect to WebSocket server
  const provider = new WebsocketProvider(
    'ws://localhost:1234',
    documentId,
    ydoc,
    {
      // Optional: authentication
      params: { userId },
    }
  )

  const awareness = provider.awareness

  // Set user info
  awareness.setLocalStateField('user', {
    name: `User ${userId}`,
    color: `#${Math.floor(Math.random() * 16777215).toString(16)}`,
  })

  // Create editor extension
  const extension = union(
    defineBasicExtension(),
    defineYjs({
      doc: ydoc,
      awareness: awareness,
      // Optional: specify custom XML fragment
      // fragment: ydoc.getXmlFragment('prosemirror'),
      // Optional: cursor plugin options
      cursor: {
        // Custom cursor rendering options
      },
      // Optional: undo plugin options
      undo: {
        // Custom undo behavior
      },
    }),
  )

  const editor = createEditor({ extension })

  // Cleanup on unmount
  const cleanup = () => {
    provider.destroy()
    ydoc.destroy()
  }

  return { editor, cleanup }
}

Collaboration with Loro

Loro is a modern CRDT library with efficient synchronization and time-travel capabilities.
1

Install dependencies

npm install loro-crdt loro-prosemirror
2

Create the Loro document

import { Loro } from 'loro-crdt'
import { CursorAwareness } from 'loro-prosemirror'

// Create a Loro document
const loro = new Loro()

// Set peer ID
loro.setPeerId(BigInt(Math.floor(Math.random() * 1000000)))

// Create awareness for cursor positions
const awareness = new CursorAwareness(loro.peerIdStr)
3

Create editor with Loro extension

import { defineBasicExtension } from 'prosekit/basic'
import { createEditor, union } from 'prosekit/core'
import { defineLoro } from 'prosekit/extensions/loro'

const extension = union(
  defineBasicExtension(),
  defineLoro({
    doc: loro,
    awareness: awareness,
  }),
)

const editor = createEditor({ extension })
4

Sync with other peers

// Export updates to send to other peers
const update = loro.exportFrom(lastVersion)

// Send update to server or other peers
sendToServer(update)

// Import updates from other peers
function receiveUpdate(update: Uint8Array) {
  loro.import(update)
}

Complete Loro Example

import { defineBasicExtension } from 'prosekit/basic'
import { createEditor, union } from 'prosekit/core'
import { defineLoro } from 'prosekit/extensions/loro'
import { Loro } from 'loro-crdt'
import { CursorAwareness } from 'loro-prosemirror'

function createLoroEditor(documentId: string) {
  // Create Loro document
  const loro = new Loro()
  loro.setPeerId(BigInt(Math.floor(Math.random() * 1000000)))

  // Create awareness
  const awareness = new CursorAwareness(loro.peerIdStr)

  // Set user info
  awareness.setLocalCursor({
    user: {
      name: `User ${loro.peerIdStr}`,
      color: `#${Math.floor(Math.random() * 16777215).toString(16)}`,
    },
    cursor: null,
  })

  // Create editor extension
  const extension = union(
    defineBasicExtension(),
    defineLoro({
      doc: loro,
      awareness: awareness,
    }),
  )

  const editor = createEditor({ extension })

  // Sync with WebSocket
  const ws = new WebSocket(`ws://localhost:1234/${documentId}`)

  ws.onmessage = (event) => {
    const update = new Uint8Array(event.data)
    loro.import(update)
  }

  // Listen for local changes
  let lastVersion = loro.version()
  loro.subscribe(() => {
    const update = loro.exportFrom(lastVersion)
    if (update.length > 0) {
      ws.send(update)
      lastVersion = loro.version()
    }
  })

  const cleanup = () => {
    ws.close()
  }

  return { editor, loro, cleanup }
}

Setting Up a WebSocket Server

For Yjs, you can use the official y-websocket server:
// server.js
import { WebSocketServer } from 'ws'
import * as Y from 'yjs'
import { setupWSConnection } from 'y-websocket/bin/utils'

const wss = new WebSocketServer({ port: 1234 })

wss.on('connection', (ws, req) => {
  setupWSConnection(ws, req)
})

console.log('WebSocket server running on ws://localhost:1234')
For Loro, you’ll need to implement your own sync protocol:
// loro-server.js
import { WebSocketServer } from 'ws'
import { Loro } from 'loro-crdt'

const wss = new WebSocketServer({ port: 1234 })
const documents = new Map()

wss.on('connection', (ws, req) => {
  const docId = req.url.slice(1)
  
  if (!documents.has(docId)) {
    documents.set(docId, new Loro())
  }
  
  const doc = documents.get(docId)
  
  // Send current state to new client
  ws.send(doc.exportSnapshot())
  
  // Broadcast updates to all clients
  ws.on('message', (data) => {
    doc.import(data)
    
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data)
      }
    })
  })
})

console.log('Loro WebSocket server running on ws://localhost:1234')

Network Providers

WebSocket Provider (Yjs)

import { WebsocketProvider } from 'y-websocket'

const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'document-id',
  ydoc,
  {
    connect: true,
    // Authentication parameters
    params: { token: 'auth-token' },
  }
)

WebRTC Provider (Yjs)

For peer-to-peer collaboration without a server:
npm install y-webrtc
import { WebrtcProvider } from 'y-webrtc'

const provider = new WebrtcProvider(
  'document-id',
  ydoc,
  {
    signaling: ['wss://signaling.yjs.dev'],
    // Optional: password protection
    password: 'my-secret-room',
  }
)

Offline Support

Both Yjs and Loro support offline editing with automatic sync when reconnected:
// Yjs: persist to IndexedDB
import { IndexeddbPersistence } from 'y-indexeddb'

const indexeddbProvider = new IndexeddbPersistence('document-id', ydoc)

indexeddbProvider.whenSynced.then(() => {
  console.log('Loaded data from IndexedDB')
})
// Loro: manual persistence
function saveToLocalStorage(loro, documentId) {
  const snapshot = loro.exportSnapshot()
  localStorage.setItem(`loro-${documentId}`, JSON.stringify(Array.from(snapshot)))
}

function loadFromLocalStorage(loro, documentId) {
  const data = localStorage.getItem(`loro-${documentId}`)
  if (data) {
    const snapshot = new Uint8Array(JSON.parse(data))
    loro.import(snapshot)
  }
}

Troubleshooting

Connection issues

If clients can’t connect:
  1. Verify WebSocket server is running
  2. Check firewall and network settings
  3. Ensure WebSocket URL is correct
  4. Check browser console for errors
  5. Verify CORS settings if needed

Sync conflicts

If you’re seeing sync conflicts:
  1. CRDTs should handle conflicts automatically
  2. Verify all clients use the same document ID
  3. Check that updates are being sent/received
  4. Ensure peer IDs are unique

Cursor positions not syncing

If cursors aren’t showing:
  1. Verify awareness is properly configured
  2. Check that user information is set
  3. Ensure cursor CSS is loaded
  4. Verify cursor plugin is included

Performance issues

For large documents or many collaborators:
  1. Use incremental updates instead of snapshots
  2. Implement debouncing for frequent changes
  3. Consider document size limits
  4. Use efficient network protocols
  5. Implement pagination for large documents

Next Steps