Real-Time Collaboration
Vizel supports real-time collaborative editing using Yjs, a CRDT-based framework that lets multiple users edit the same document simultaneously without conflicts.
Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client A │ │ Client B │ │ Client C │
│ (Vizel + │ │ (Vizel + │ │ (Vizel + │
│ Yjs) │ │ Yjs) │ │ Yjs) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────┬───────┴────────────────────┘
│
┌───────▼───────┐
│ Yjs Server │
│ (y-websocket) │
└───────────────┘Vizel provides:
- History exclusion — Automatically disables the built-in History extension when you enable collaboration (Yjs provides its own undo manager)
- State tracking — Framework hooks/composables/runes for tracking connection status, sync state, and peer count
- Lifecycle management — Automatic setup and cleanup of event listeners
Prerequisites
Install the required peer dependencies:
npm install yjs y-websocket @tiptap/extension-collaboration @tiptap/extension-collaboration-cursorVersion Compatibility
Ensure your @tiptap/extension-collaboration version is compatible with the @tiptap/core version used by Vizel. Check the Tiptap documentation for version requirements.
Setup
1. Start a Yjs WebSocket Server
You need a Yjs-compatible WebSocket server for synchronization. The simplest option uses y-websocket:
npx y-websocketThis starts a server on ws://localhost:1234. For production, see Server Setup below.
2. Configure the Editor
import { useState } from "react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import {
VizelEditor,
VizelBubbleMenu,
useVizelEditor,
useVizelCollaboration,
} from "@vizel/react";
function CollaborativeEditor() {
const user = { name: "Alice", color: "#ff0000" };
// Create Yjs document and WebSocket provider
const [doc] = useState(() => new Y.Doc());
const [provider] = useState(
() => new WebsocketProvider("ws://localhost:1234", "my-document", doc)
);
// Track collaboration state
const { isConnected, isSynced, peerCount } = useVizelCollaboration(
() => provider,
{ user }
);
// Create editor with collaboration enabled
const editor = useVizelEditor({
features: { collaboration: true },
extensions: [
Collaboration.configure({ document: doc }),
CollaborationCursor.configure({ provider, user }),
],
});
return (
<div>
<div className="status-bar">
<span>{isConnected ? "🟢 Connected" : "🔴 Disconnected"}</span>
<span>{isSynced ? "Synced" : "Syncing..."}</span>
<span>{peerCount} peer(s)</span>
</div>
<VizelEditor editor={editor} />
{editor && <VizelBubbleMenu editor={editor} />}
</div>
);
}<script setup lang="ts">
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import {
VizelEditor,
VizelBubbleMenu,
useVizelEditor,
useVizelCollaboration,
} from "@vizel/vue";
const user = { name: "Alice", color: "#ff0000" };
// Create Yjs document and WebSocket provider
const doc = new Y.Doc();
const provider = new WebsocketProvider(
"ws://localhost:1234",
"my-document",
doc
);
// Track collaboration state
const { isConnected, isSynced, peerCount } = useVizelCollaboration(
() => provider,
{ user }
);
// Create editor with collaboration enabled
const editor = useVizelEditor({
features: { collaboration: true },
extensions: [
Collaboration.configure({ document: doc }),
CollaborationCursor.configure({ provider, user }),
],
});
</script>
<template>
<div>
<div class="status-bar">
<span>{{ isConnected ? "🟢 Connected" : "🔴 Disconnected" }}</span>
<span>{{ isSynced ? "Synced" : "Syncing..." }}</span>
<span>{{ peerCount }} peer(s)</span>
</div>
<VizelEditor :editor="editor" />
<VizelBubbleMenu v-if="editor" :editor="editor" />
</div>
</template><script lang="ts">
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import {
VizelEditor,
VizelBubbleMenu,
createVizelEditor,
createVizelCollaboration,
} from "@vizel/svelte";
const user = { name: "Alice", color: "#ff0000" };
// Create Yjs document and WebSocket provider
const doc = new Y.Doc();
const provider = new WebsocketProvider(
"ws://localhost:1234",
"my-document",
doc
);
// Track collaboration state
const collab = createVizelCollaboration(
() => provider,
{ user }
);
// Create editor with collaboration enabled
const editor = createVizelEditor({
features: { collaboration: true },
extensions: [
Collaboration.configure({ document: doc }),
CollaborationCursor.configure({ provider, user }),
],
});
</script>
<div>
<div class="status-bar">
<span>{collab.isConnected ? "🟢 Connected" : "🔴 Disconnected"}</span>
<span>{collab.isSynced ? "Synced" : "Syncing..."}</span>
<span>{collab.peerCount} peer(s)</span>
</div>
<VizelEditor editor={editor.current} />
{#if editor.current}
<VizelBubbleMenu editor={editor.current} />
{/if}
</div>API Reference
Options
The collaboration hook/composable/rune accepts these options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable collaboration state tracking |
user | { name, color } | Required | Current user info for cursor display |
onConnect | () => void | — | Runs when the client connects to the server |
onDisconnect | () => void | — | Runs when the client disconnects |
onSynced | () => void | — | Runs when initial sync completes |
onError | (error) => void | — | Runs when an error occurs |
onPeersChange | (count) => void | — | Runs when the peer count changes |
Return Values
| Property | Type | Description |
|---|---|---|
isConnected | boolean | Whether connected to the server |
isSynced | boolean | Whether initial document sync is complete |
peerCount | number | Number of connected peers (including self) |
error | Error | null | Last error that occurred |
connect() | () => void | Connect to the server |
disconnect() | () => void | Disconnect from the server |
updateUser() | (user) => void | Update cursor information |
Feature Flag
Setting features.collaboration to true in the editor options disables the built-in History extension. This is necessary because Yjs provides its own undo/redo mechanism through Y.UndoManager.
const editor = useVizelEditor({
features: {
collaboration: true, // Disables History extension
},
});Key Concepts
How Collaboration Works
CRDT (Conflict-free Replicated Data Type) — Yjs uses CRDTs to merge concurrent edits without conflicts. Each client can edit independently, and Yjs automatically merges changes.
Awareness — The Yjs Awareness protocol tracks ephemeral state like cursor positions, user names, and colors. This state is separate from the document and is not persisted.
History — When you enable collaboration, you must disable the built-in Tiptap History extension because Yjs provides
Y.UndoManager, which understands CRDT operations. The standard History extension would conflict with collaborative edits.
Offline Support
Yjs automatically handles offline scenarios:
- Yjs stores edits made offline locally
- When reconnecting, Yjs automatically syncs and merges changes
- No data loss occurs even with extended offline periods
Server Setup
Development Server
For local development, use the built-in y-websocket server:
npx y-websocketProduction Server
For production deployments, create a custom server:
// server.js
import { WebSocketServer } from "ws";
import { setupWSConnection } from "y-websocket/bin/utils";
const wss = new WebSocketServer({ port: 1234 });
wss.on("connection", (ws, req) => {
setupWSConnection(ws, req);
});
console.log("Yjs WebSocket server running on ws://localhost:1234");Persistence
To persist documents on the server, use y-websocket with LevelDB:
HOST=0.0.0.0 PORT=1234 YPERSISTENCE=./yjs-docs npx y-websocketOr configure persistence programmatically:
import { LeveldbPersistence } from "y-leveldb";
const persistence = new LeveldbPersistence("./yjs-docs");
// Pass to y-websocket server configurationAlternative Providers
Yjs supports multiple transport providers:
| Provider | Package | Use Case |
|---|---|---|
| WebSocket | y-websocket | Standard server-client setup |
| WebRTC | y-webrtc | Peer-to-peer, no server needed |
| Hocuspocus | @hocuspocus/provider | Feature-rich, authentication support |
Troubleshooting
History Extension Conflict
Problem: Undo/redo behaves unexpectedly with collaboration enabled.
Solution: Set features.collaboration to true in your editor options. This disables the built-in History extension that conflicts with Yjs's undo manager.
Connection Issues
Problem: WebSocket connection fails or keeps disconnecting.
Solution:
- Verify that the WebSocket server is running
- Check that the server URL is correct (including protocol
ws://orwss://) - For production, confirm that your reverse proxy supports WebSocket connections
- Check CORS settings if the server is on a different origin
Cursor Colors Not Showing
Problem: Remote cursors appear but without colors.
Solution: Pass the user object with both name and color properties to both CollaborationCursor.configure() and the collaboration hook/composable/rune.