Scrivr
Examples

Real-time Collaboration

Full backend and frontend setup for multiplayer editing with Scrivr.

This example shows how to set up a real-time collaborative editor using @scrivr/plugins and a Hocuspocus-compatible WebSocket backend.


How It Works

Scrivr's collaboration layer uses Yjs CRDTs to merge concurrent edits. The library owns the WebSocket connection — you don't need to manage a provider on the client.

  • Collaboration.configure({ url, name }) — connects to your server and binds the document
  • CollaborationCursor.configure({ user }) — renders remote user cursors on the overlay canvas

Backend Setup

Scrivr's collaboration protocol is compatible with any Hocuspocus server. You can self-host Hocuspocus directly:

pnpm add @hocuspocus/server
server.ts
import { Server } from '@hocuspocus/server';

const server = Server.configure({
  port: 1234,
  // Optional: persist documents to a database
  async onLoadDocument({ documentName }) {
    return await db.load(documentName);
  },
  async onStoreDocument({ documentName, document }) {
    await db.save(documentName, document);
  },
});

server.listen();
console.log('Collaboration server running on ws://localhost:1234');

Frontend Setup

CollaborativeEditor.tsx
import { useScrivrEditor, Scrivr, useScrivrState, StarterKit } from '@scrivr/react';
import { Collaboration, CollaborationCursor } from '@scrivr/plugins';

interface Props {
  documentId: string;
  user: { name: string; color: string };
}

export function CollaborativeEditor({ documentId, user }: Props) {
  const editor = useScrivrEditor(
    {
      extensions: [
        StarterKit.configure({ history: false }), // Y.UndoManager replaces PM history
        Collaboration.configure({
          url: import.meta.env.VITE_WS_URL, // e.g. 'ws://localhost:1234'
          name: documentId,
        }),
        CollaborationCursor.configure({ user }),
      ],
    },
    [documentId], // re-initialize when document changes
  );

  const loadingState = useScrivrState({
    editor,
    selector: ({ editor }) => editor?.loadingState,
  });

  if (loadingState === 'syncing') {
    return <div>Connecting…</div>;
  }

  return <Scrivr editor={editor} style={{ height: '100vh' }} />;
}

Environment variable required: VITE_WS_URL — the WebSocket URL of your Hocuspocus server.

Authentication — coming soon. An onAuthenticate callback will be added to Collaboration.configure() so you can attach tokens or headers to the WebSocket handshake before the connection is established:

// Future API — not yet available
Collaboration.configure({
  url: import.meta.env.VITE_WS_URL,
  name: documentId,
  onAuthenticate: () => ({
    token: getAuthToken(),
  }),
})

loadingState

The editor exposes a loadingState property to track the connection lifecycle:

ValueMeaning
'syncing'WebSocket connected, waiting for initial document sync from server.
'rendering'Document synced, layout engine running for the first time.
'ready'Layout complete — document is fully visible.

Use loadingState to show a loading spinner or skeleton UI while the document loads.


Cursor Colours

Each connected user should have a distinct colour. A common pattern is to assign colours from a palette based on user ID:

const CURSOR_COLORS = [
  '#f7b731', '#26de81', '#45aaf2', '#fd9644', '#a55eea',
];

function getCursorColor(userId: string): string {
  let hash = 0;
  for (const char of userId) hash = (hash * 31 + char.charCodeAt(0)) & 0xffffffff;
  return CURSOR_COLORS[Math.abs(hash) % CURSOR_COLORS.length];
}
CollaborationCursor.configure({
  user: {
    name: currentUser.name,
    color: getCursorColor(currentUser.id),
  },
})

On this page