Upgrade Guide
Upgrading to 4.0 from 3.x
Hocuspocus v4 brings cross-runtime support (Bun, Deno, Cloudflare Workers, Node with uWebSockets), improved type safety via a generic Context type, and structured transaction origins.
The wire protocol is backward compatible in both directions: v3 providers can connect to v4 servers and vice versa. See the v4 release notes for full details.
1. Update dependencies
npm install @hocuspocus/server@^4.0.0 @hocuspocus/provider@^4.0.0Node.js requirement: v4 requires Node.js 22 or later.
If you use the SQLite extension, replace sqlite3 with better-sqlite3:
npm uninstall sqlite3
npm install @hocuspocus/extension-sqlite@^4.0.0 better-sqlite3
npm install -D @types/better-sqlite3Existing SQLite database files are fully compatible — no data migration needed.
2. Web-standard Request and Headers (Breaking)
All hook payloads now use the web-standard Request and Headers objects instead of Node.js IncomingMessage and IncomingHttpHeaders.
Before (v3):
async onAuthenticate({ request, requestHeaders }) {
const token = requestHeaders['authorization']
const ip = requestHeaders['x-forwarded-for']
const url = request.url
}After (v4):
async onAuthenticate({ request, requestHeaders }) {
const token = requestHeaders.get('authorization')
const ip = requestHeaders.get('x-forwarded-for')
const url = request.url
}request.socket.remoteAddress is no longer available — use x-forwarded-for or x-real-ip headers from your reverse proxy instead.
The onUpgrade and onRequest hooks still use Node.js IncomingMessage/ServerResponse because they operate at the HTTP level before the WebSocket upgrade.
3. onStoreDocument payload (Breaking)
The onStoreDocument and afterStoreDocument payloads have been restructured. Fields tied to a specific connection have been removed since store hooks can now be triggered by non-connection sources.
Before (v3):
async onStoreDocument({ context, requestHeaders, requestParameters, socketId, transactionOrigin, document, documentName, clientsCount, instance }) {
// ...
}After (v4):
async onStoreDocument({ lastContext, lastTransactionOrigin, document, documentName, clientsCount, instance }) {
// `context` → `lastContext`
// `transactionOrigin` → `lastTransactionOrigin`
// `requestHeaders`, `requestParameters`, `socketId` — removed. Access via `lastContext` if needed.
}4. onAwarenessUpdate payload (Breaking)
Connection-specific fields are removed and replaced with the source of the update.
After (v4):
async onAwarenessUpdate({
transactionOrigin, // new: structured origin
connection, // new: optional, the connection that triggered the update
document,
documentName,
added, updated, removed,
awareness, // new: the Awareness instance
states,
}) {
// Access context through the connection if needed
const context = connection?.context
}5. Transaction origin (Breaking)
Transaction origins are now structured objects rather than raw values.
Before (v3):
async onChange({ transactionOrigin }) {
if (transactionOrigin === '__hocuspocus__redis__origin__') {
// came from Redis
}
if (transactionOrigin instanceof Connection) {
// came from a client connection
}
}After (v4):
import { isTransactionOrigin } from '@hocuspocus/server'
async onChange({ transactionOrigin }) {
if (isTransactionOrigin(transactionOrigin)) {
switch (transactionOrigin.source) {
case 'redis':
// came from Redis
break
case 'connection':
// transactionOrigin.connection is available
break
case 'local':
// came from server-side code (e.g. DirectConnection)
break
}
}
}6. WebSocket options (Breaking)
WebSocket options are now passed inside the configuration object instead of as a separate parameter.
Before (v3):
const server = new Server(
{ port: 8080, extensions: [...] },
{ maxPayload: 1024 * 1024 }, // ws options as 2nd arg
)After (v4):
const server = new Server({
port: 8080,
extensions: [...],
websocketOptions: { maxPayload: 1024 * 1024 },
})7. WebSocket type changes (Breaking)
If your code references the WebSocket type from the ws package, update to WebSocketLike:
import type { WebSocketLike } from '@hocuspocus/server'
const ws: WebSocketLike = connection.webSocketWebSocketLike is a minimal interface with send, close, and readyState — compatible with all supported runtimes.
8. Custom handleConnection integrations (Breaking)
If you call handleConnection() directly (e.g. for Express/Koa integration), the signature has changed:
After (v4):
wss.on('connection', (ws, request: Request) => {
const clientConnection = hocuspocus.handleConnection(ws, request, context)
// clientConnection is now returned for programmatic access
})requestmust be a web-standardRequest(notIncomingMessage)- The method now returns a
ClientConnectioninstance - The WebSocket no longer needs to be from the
wspackage — anyWebSocketLikeworks - If you are not using the built-in
Serverclass, you are responsible for callingclientConnection.handleMessage(data)andclientConnection.handleClose(event)
9. Provider CloseEvent shape (Breaking)
The CloseEvent passed to onClose callbacks no longer includes target and type. Only code and reason remain.
onClose({ event }) {
console.log(event.code, event.reason)
// event.target and event.type are no longer available
}10. Provider ws package types removed (TypeScript only)
The provider no longer re-exports Event, MessageEvent, or CloseEvent from the ws package. Import them from @hocuspocus/common or use web-standard types instead.
11. Timeout change (Non-breaking)
The default connection timeout has increased from 30 seconds to 60 seconds. To keep the old behavior:
const server = new Server({
timeout: 30_000,
})Summary checklist
Server
- Update to Node.js 22+
- Update all
@hocuspocus/*packages to v4 - Replace
requestHeaders['key']withrequestHeaders.get('key')in all hooks - Replace
request.socket.remoteAddresswith proxy headers (x-forwarded-for) - Update
onStoreDocumenthandlers:context→lastContext, removerequestHeaders/requestParameters/socketId - Update
onAwarenessUpdatehandlers if used - Replace
WebSockettype imports fromwswithWebSocketLikefrom@hocuspocus/server - Move
websocketOptionsinto theServerconfiguration object - Update transaction origin checks to use
isTransactionOrigin()and.source - If using SQLite: replace
sqlite3withbetter-sqlite3 - If using custom
handleConnection: update to new signature andRequesttype
Provider
- Update
@hocuspocus/providerto v4 - Remove references to
event.target/event.typeinonClosehandlers - Update TypeScript imports that relied on
wstypes from the provider
Upgrading to 3.0 from 2.x (Provider)
The TiptapCollabProvider has been moved to @tiptap-pro/provider.
When using multiplexing, the providers now need to explicitly call attach to attach to the websocket.
import {
TiptapCollabProvider,
TiptapCollabProviderWebsocket
} from "@tiptap-pro/provider";
const socket = new TiptapCollabProviderWebsocket({
appId: '', // or `url` if using `HocuspocusProviderWebsocket`
})
const provider1 = new TiptapCollabProvider({
websocketProvider: socket,
name: 'document1',
token: '',
})
provider1.attach() // when passing a socket manually, you need to call attach explicitlyMore information can be found here: https://github.com/ueberdosis/hocuspocus/releases/tag/v3.1.0
Upgrading to 3.0 from 2.x (Server)
With the upgrade to the new version, the initialization of hocuspocus has changed. As described on the usage side, there are two ways on how you can use hocuspocus. With the built-in server. Or like a library with other frameworks (like express). To make things simpler and enable more features in the future, we separated classes and put the server into its own class.
Usage with .configure()
It is no longer possible to use hocuspocus with .configure(). You always have to create a new instance by yourself.
Old Way
import { Server } from "@hocuspocus/server";
const server = Server.configure({
port: 1234,
});
server.listen();New Way
import { Server } from "@hocuspocus/server";
const server = new Server({
port: 1234,
});
server.listen();Notice, that the import has not changed. The configuration options stay the same here.
Usage of Hocuspocus without built-in server
If you have used Hocuspocus without the built-in server before, you have to update your setup as well.
Old Way
import { Server } from "@hocuspocus/server";
const server = Server.configure({
// ...
});New Way
import { Hocuspocus } from "@hocuspocus/server";
const hocuspocus = new Hocuspocus({
// ...
});
// You still use handleConnection as you did before.
hocuspocus.handleConnection(...);Notice the change of the import from Server to Hocuspocus as well as the initialization with new Hocuspocus().
See examples for more on that.
Change of the servers listen signature
The .listen() function of the server was quite versatile. We simplified the signature of it while you can still reach
the same behavior as before.
Old Signature
async listen(
portOrCallback: number | ((data: onListenPayload) => Promise<any>) | null = null,
callback: any = null,
): Promise<Hocuspocus>New Signature
async listen(port?: number, callback: any = null): Promise<Hocuspocus>The listen method still returns a Promise which will be resolved to Hocuspocus, if nothing fails.
Both the callbacks you could provide in the old version were added to the onListen hook. This is still the case with
the callback on the new version. But you can't provide just a callback on the first parameter anymore. If you just want
to add a callback you also still can add it within the configuration of the server.
import { Server } from "@hocuspocus/server";
const server = new Server({
async onListen(data) {
console.log(`Server is listening on port "${data.port}"!`);
},
});
server.listen()