Real-time collaboration is powered by Hocuspocus — a WebSocket server built on top of Yjs. The stack:
Browser (Tiptap + yjs + @hocuspocus/provider) │ WebSocket wss://collab.ivory.finance ▼Hocuspocus Server (Node.js — ghcr.io/vennroad/ivory-hocuspocus) │ JWT verification + ACL check ├─► POST /v1/editor/hocuspocus/auth │ │ Load/save Yjs CRDT binary ├─► SELECT yjs_state FROM documents WHERE id=$1 └─► PUT /v1/editor/documents/{doc_id}/yjs
Each document has a room identified by its UUID. Multiple clients connect to the same room and Hocuspocus merges their edits using the Yjs CRDT algorithm.
Use the document UUID as the Hocuspocus room name. Pass your Ivory JWT as the token — the server calls the auth webhook to verify it before admitting the connection.
import { HocuspocusProvider } from "@hocuspocus/provider";import * as Y from "yjs";const ydoc = new Y.Doc();const provider = new HocuspocusProvider({ url: "wss://collab.ivory.finance", name: documentId, // the document UUID from GET /v1/editor/documents document: ydoc, token: IVORY_JWT, // Ivory Finance JWT (NOT the API key) onAuthenticated: () => { console.log("Connected and authenticated"); }, onAuthenticationFailed: ({ reason }) => { console.error("Auth failed:", reason); }, onDisconnect: () => { console.log("Disconnected — will auto-reconnect"); },});
The Yjs CRDT is the source of truth for real-time edits. The REST doc_ast field must be kept in sync for the AI to see the latest content. Trigger the PATCH on blur, explicit save, or every 30 seconds:
On every new WebSocket connection, Hocuspocus calls the auth webhook before allowing access. You don’t call this from the browser — it’s invoked by the Hocuspocus server process.
Hocuspocus automatically saves the Yjs binary after each document update. Do not call this from your frontend — it is internal, authenticated via a shared secret.
# Internal — called by Hocuspocus server, not the browsercurl -X PUT "https://api.ivory.finance/v1/editor/documents/doc_01JK4.../yjs" \ -H "X-Hocuspocus-Secret: $HOCUSPOCUS_SECRET" \ -H "Content-Type: application/json" \ -d '{ "yjs_state": "<base64-encoded Yjs binary>", "updated_by_type": "user", "updated_by_id": "user_abc123" }'
yjs_state vs doc_ast — two parallel representations:
yjs_state — Yjs CRDT binary. Source of truth for the real-time editor. Hocuspocus manages it exclusively.
doc_ast — Tiptap JSON. Source of truth for agents and REST consumers. Synced via PATCH /v1/editor/documents/{id}.