Documentation Index Fetch the complete documentation index at: https://superdoc-nick-sd-3220-overlapping-suggestion-contract.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Hocuspocus is TipTap’s open-source Yjs WebSocket server. It’s a mature, battle-tested option for self-hosted collaboration.
Setup
Server
npm install @hocuspocus/server yjs
import { Server } from "@hocuspocus/server" ;
import * as Y from "yjs" ;
const server = Server . configure ({
port: 1234 ,
async onLoadDocument ( data ) {
// Load the stored Yjs update from your database.
const state = await db . getDocument ( data . documentName );
if ( state ) {
Y . applyUpdate ( data . document , state );
}
return data . document ;
},
async onStoreDocument ( data ) {
// Store the Yjs document as a binary update.
const state = Y . encodeStateAsUpdate ( data . document );
await db . saveDocument ( data . documentName , Buffer . from ( state ));
},
async onAuthenticate ( data ) {
// Validate token
const user = await validateToken ( data . token );
if ( ! user ) {
throw new Error ( "Unauthorized" );
}
return { user };
},
});
server . listen ();
Client
Use the provider-agnostic API to connect SuperDoc:
npm install @hocuspocus/provider yjs
import { HocuspocusProvider } from "@hocuspocus/provider" ;
import * as Y from "yjs" ;
import { SuperDoc } from "superdoc" ;
const ydoc = new Y . Doc ();
const provider = new HocuspocusProvider ({
url: "ws://localhost:1234" ,
name: "document-123" ,
document: ydoc ,
token: "auth-token" , // Optional
});
// Wait for sync before creating editor
provider . on ( "synced" , () => {
const superdoc = new SuperDoc ({
selector: "#editor" ,
documentMode: "editing" ,
user: {
name: "John Smith" ,
email: "john@example.com" ,
},
modules: {
collaboration: { ydoc , provider },
},
});
});
SuperDoc JS always uses the same collaboration contract, regardless of provider: modules.collaboration = { ydoc, provider }.
Seed from DOCX only for empty rooms
A DOCX file can seed a new collaboration room. After that, the Yjs document is
the source of truth.
Wait for Hocuspocus to sync before you decide whether the room is empty. If you
check before sync, the client can look empty while the server still has stored
Yjs state.
function hasSuperDocContent ( ydoc : Y . Doc ) {
return (
ydoc . getXmlFragment ( "supereditor" ). length > 0 ||
ydoc . getMap ( "parts" ). size > 0 ||
ydoc . getMap ( "meta" ). has ( "docx" )
);
}
provider . on ( "synced" , () => {
const shouldSeedFromDocx = ! hasSuperDocContent ( ydoc );
new SuperDoc ({
selector: "#editor" ,
documentMode: "editing" ,
... ( shouldSeedFromDocx
? {
document: {
type: "docx" ,
url: "/initial.docx" ,
isNewFile: true ,
},
}
: {}),
user: {
name: "John Smith" ,
email: "john@example.com" ,
},
modules: {
collaboration: { ydoc , provider },
},
});
});
If your backend already knows whether a room has stored Yjs state, use that
metadata. The important part is the order: sync first, inspect the Yjs document
or room metadata, then decide whether to pass the DOCX seed.
React example
import { useEffect , useRef , useState } from "react" ;
import { HocuspocusProvider } from "@hocuspocus/provider" ;
import * as Y from "yjs" ;
import { SuperDoc } from "superdoc" ;
import "superdoc/style.css" ;
function hasSuperDocContent ( ydoc : Y . Doc ) {
return (
ydoc . getXmlFragment ( "supereditor" ). length > 0 ||
ydoc . getMap ( "parts" ). size > 0 ||
ydoc . getMap ( "meta" ). has ( "docx" )
);
}
export default function Editor () {
const superdocRef = useRef < SuperDoc | null >( null );
const [ users , setUsers ] = useState < any []>([]);
useEffect (() => {
const ydoc = new Y . Doc ();
const provider = new HocuspocusProvider ({
url: "ws://localhost:1234" ,
name: "my-document" ,
document: ydoc ,
});
provider . on ( "synced" , () => {
if ( superdocRef . current ) return ;
const shouldSeedFromDocx = ! hasSuperDocContent ( ydoc );
superdocRef . current = new SuperDoc ({
selector: "#superdoc" ,
documentMode: "editing" ,
... ( shouldSeedFromDocx
? {
document: {
type: "docx" ,
url: "/initial.docx" ,
isNewFile: true ,
},
}
: {}),
user: {
name: `User ${ Math . floor ( Math . random () * 1000 ) } ` ,
email: "user@example.com" ,
},
modules: {
collaboration: { ydoc , provider },
},
onAwarenessUpdate : ({ states }) => {
setUsers ( states . filter (( s ) => s . user ));
},
});
});
return () => {
superdocRef . current ?. destroy ();
provider . destroy ();
};
}, []);
return (
< div >
< div className = "users" >
{ users . map (( u , i ) => (
< span key = { i } style = { { background: u . user ?. color } } >
{ u . user ?. name }
</ span >
)) }
</ div >
< div id = "superdoc" style = { { height: "100vh" } } />
</ div >
);
}
Server configuration
Basic options
Server . configure ({
port: 1234 ,
timeout: 30000 , // Connection timeout
debounce: 2000 , // Debounce document saves
maxDebounce: 10000 , // Max wait before save
quiet: false , // Disable logging
});
Hooks
Hook Purpose onLoadDocumentLoad document from storage onStoreDocumentSave document to storage onAuthenticateValidate user tokens onChangeReact to document changes onConnectHandle new connections onDisconnectHandle disconnections
Persistence example
import { Server } from "@hocuspocus/server" ;
import { Database } from "@hocuspocus/extension-database" ;
const server = Server . configure ({
extensions: [
new Database ({
fetch : async ({ documentName }) => {
const doc = await db . findOne ({ name: documentName });
return doc ?. data || null ;
},
store : async ({ documentName , state }) => {
await db . upsert ({ name: documentName }, { data: state });
},
}),
],
});
Provider options
const provider = new HocuspocusProvider ({
url: "ws://localhost:1234" ,
name: "document-id" ,
document: ydoc ,
// Optional
token: "auth-token" ,
awareness: awareness , // Custom awareness instance
connect: true , // Auto-connect on create
preserveConnection: true , // Keep connection on destroy
broadcast: true , // Broadcast changes to tabs
});
Events
Provider events
// Sync status
provider . on ( "synced" , () => {
console . log ( "Document synced" );
});
// Connection status
provider . on ( "status" , ({ status }) => {
// 'connecting' | 'connected' | 'disconnected'
});
// Authentication
provider . on ( "authenticationFailed" , ({ reason }) => {
console . error ( "Auth failed:" , reason );
});
Production deployment
Docker
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 1234
CMD [ "node" , "server.js" ]
With Redis (scaling)
import { Server } from "@hocuspocus/server" ;
import { Redis } from "@hocuspocus/extension-redis" ;
Server . configure ({
extensions: [
new Redis ({
host: "localhost" ,
port: 6379 ,
}),
],
});
Resources
Hocuspocus Docs Official documentation
Working example Complete source code
Next steps
Client Configuration All SuperDoc collaboration options
Self-hosted overview Compare Hocuspocus, YHub, and the SuperDoc Yjs reference server