Use your own provider
Keep an existing Hocuspocus or other Yjs provider setup, either through built-in settings or a provider extension.
SuperDoc owns the browser's shared document. It creates the Y.Doc inside a worker and opens the provider connection
for it. You cannot hand it a Y.Doc or provider instance you already created. You can keep your server, your provider
library, and your authentication. Pick one of two ways to connect them.
Choose a path
| Built-in provider | Provider extension | |
|---|---|---|
| Use it when | Your server speaks Hocuspocus, y-websocket, or Liveblocks. | You need your own connection code, for example custom provider options. |
| What you write | Connection settings in document.collaboration. | A worker module with an adapter, plus settings that name it. |
| Worker | SuperDoc's default worker. | Your worker, passed as workerUrls.collaboration. |
Shared-room check on create | Yes. | No. Readiness means local readiness only. |
Start with a built-in provider. Most existing Hocuspocus setups need only settings. Write an extension only when the built-in settings cannot express your connection.
In both paths you reuse your backend and provider library. You do not reuse a provider instance: SuperDoc creates the connection, and it ends the connection when the editor is destroyed.
Move a Hocuspocus setup to built-in settings
A SuperDoc v1 integration created its own provider:
// SuperDoc v1
const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({
url: 'wss://collab.example.com',
name: 'contract-42',
document: ydoc,
token,
});
new SuperDoc({ selector: '#editor', document: '/contract.docx', modules: { collaboration: { ydoc, provider } } });In v2, pass the same settings and let SuperDoc create the provider:
import { SuperDoc, type DocumentCollaborationConfig } from 'superdoc';
const collaboration = {
providerType: 'hocuspocus',
serverUrl: 'wss://collab.example.com',
documentId: 'contract-42',
token: () => getAccessToken(),
roomMode: 'join',
} satisfies DocumentCollaborationConfig;
new SuperDoc({ selector: '#editor', document: { url: '/contract.docx', collaboration } });tokencan be a string or a function. SuperDoc calls the function for every connection, including reconnects, so it can return a fresh token.- The room name on your server changes.
documentId: 'contract-42'reaches Hocuspocus assd2/v2.1/contract-42. Update any server hooks that authorize or store rooms by name. See Control access to a room. - Existing v1 room data is not v2 room data. See Upgrade a document for moving it.
Write a provider extension
An extension has three parts:
- An adapter that connects the
Y.DocSuperDoc gives it. - A collaboration worker that registers the adapter under an ID.
- Settings that name that ID and point SuperDoc at your worker.
1. Write the adapter and worker
Create a worker module, for example collaboration-worker.ts. It registers every adapter your application uses. Built-in providers keep working in the same
worker.
import { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider';
import {
bootstrapSuperDocCollaborationWorker,
type SuperDocCollaborationProviderFactory,
} from 'superdoc/collaboration-worker';
interface AppProviderOptions {
url: string;
tenant: string;
}
const appHocuspocus: SuperDocCollaborationProviderFactory = ({ providerOptions, token }) => {
const { url, tenant } = providerOptions as AppProviderOptions;
return {
providerFamily: 'hocuspocus',
attach({ ydoc, providerRoomName, onSynced, onDegraded, onFailed, onStateless }) {
let synced = false;
const socket = new HocuspocusProviderWebsocket({ url });
const provider = new HocuspocusProvider({
websocketProvider: socket,
name: providerRoomName,
document: ydoc,
token,
parameters: { tenant },
onSynced() {
synced = true;
onSynced();
},
onDisconnect() {
if (synced) onDegraded();
},
onAuthenticationFailed() {
onFailed({ reason: 'authentication_failed' });
},
onClose({ event }) {
if (event.code === 4401 || event.code === 4403) onFailed({ code: event.code });
},
onStateless({ payload }) {
onStateless(payload);
},
});
return {
awareness: provider.awareness,
sendStateless: (message) => provider.sendStateless(message),
disconnect: () => provider.disconnect(),
destroy() {
provider.destroy();
socket.destroy();
},
};
},
};
};
bootstrapSuperDocCollaborationWorker({
providerAdapters: { 'app-hocuspocus': appHocuspocus },
});- Use the
ydocandproviderRoomNameyou are given. Do not create your ownY.Docor build your own room name. providerFamilymust be'hocuspocus','y-websocket', or'liveblocks'. Choose the protocol your provider speaks. SuperDoc uses it to decide how authentication and reconnects behave.@hocuspocus/provideris a peer dependency ofsuperdoc. Install it in your application.- Your worker must load a single copy of
yjs. If the console warnsYjs was already imported, your provider and SuperDoc are using different copies and the room will not open. Deduplicateyjsin your bundler.
2. Point SuperDoc at the worker and adapter
Build the worker as a same-origin module worker and pass its URL. With Vite, import it with ?worker&url:
import { SuperDoc, type DocumentCollaborationConfig } from 'superdoc';
import collaborationWorkerUrl from './collaboration-worker.ts?worker&url';
const collaboration = {
providerType: 'extension',
adapterId: 'app-hocuspocus',
documentId: 'contract-42',
providerOptions: { url: 'wss://collab.example.com', tenant: 'acme' },
token: () => getAccessToken(),
roomMode: 'join',
} satisfies DocumentCollaborationConfig;
new SuperDoc({
selector: '#editor',
document: { url: '/contract.docx', collaboration },
workerUrls: { collaboration: collaborationWorkerUrl },
});adapterIdmust match a key inproviderAdapters.providerOptionsis copied into the worker, so it must be plain data: strings, numbers, arrays, and objects. It cannot hold functions, class instances, or a provider.tokenstays on the page. The adapter receives it as a string, or as a function that asks the page for a fresh token on every call.
What an extension must handle
| Area | What to do |
|---|---|
| Sync | Call onSynced() when the provider has synced. Call it again after each reconnect so the room returns to synced. |
| Outages | Call onDegraded() when the connection drops after a sync. Let the provider reconnect. |
| Failure | Call onFailed(detail) only for errors that will not recover. Use { reason: 'authentication_failed' } for a rejected credential; onException reports it as access-denied. |
| Authentication | Pass token to your provider. If it is a function, the provider calls it on each connection. |
| Presence | Return the provider's Yjs awareness. Without it, presence and cursors do not appear. |
| Custom messages | Return sendStateless to support superdoc.provider.sendStateless(message). Forward incoming messages to onStateless. |
| Create and join | roomMode works, but create does not check that other editors can see the new room. Do not treat collaboration-ready as a signal that others can join. |
| Cleanup | disconnect() closes the connection. destroy() releases everything the transport created. SuperDoc calls them when the editor is destroyed. |
Server-to-page messages are not available yet. Your page can send a stateless message, but a message from the server
reaches only your adapter's onStateless, inside the worker.
Collaboration syncs the shared document. It does not save a DOCX or store the room on your server. See Save and restore a room.
Limits
- An extension works only in a worker that registers it. The default worker has no extensions.
- SuperDoc supports the three provider families above. Other Yjs providers may work through an extension if they follow one of those protocols, but SuperDoc does not test them.
superdoc.provideris not your provider instance. In v2 it only offerssendStateless(message).superdoc.ydocis not set.
Errors you might see
| Message | Fix |
|---|---|
...cannot use an external { ydoc, provider } pair... | Remove ydoc and provider. Use built-in settings or an extension. |
...cannot use modules.collaboration... | Move the connection to document.collaboration. |
...provider extensions require a non-empty collaboration.adapterId | Set adapterId to a key in your worker's providerAdapters. |
...provider extension options must be structured-clone-safe | Remove functions and class instances from providerOptions. |
collaboration provider extension '...' is not registered in this worker | Pass your worker as workerUrls.collaboration, and check that the IDs match. |
...returned an unsupported providerFamily | Set providerFamily to 'hocuspocus', 'y-websocket', or 'liveblocks'. |