This release includes 10 breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
+2 more
Affected surfaces
Summary
AI summaryBroad release touches Server Changes, Server, non-breaking, and Provider.
Full changelog
Hocuspocus v4.0 Release Notes
Hocuspocus v4 is a major release that brings cross-runtime support, improved type safety, and important bug fixes. This release focuses on making Hocuspocus run beyond Node.js -- on Bun, Deno, Cloudflare Workers, and Node with uWebSockets -- while improving the developer experience with generic Context typing and structured transaction origins.
Backward Compatibility
A v3 provider can connect to a v4 server, and a v4 provider can connect to a v3 server. The wire protocol remains compatible in both directions:
- v3 provider -> v4 server: The server accepts plain document names (no session routing key), does not require Pong responses, and handles auth messages without a provider version string.
- v4 provider -> v3 server: The provider defaults to
sessionAwareness: false, so it sends plain document names. The extra version string in the auth message is ignored by the v3 server as trailing data. The provider does not require server-initiated Ping messages. - Session awareness caveat: If
sessionAwareness: trueis explicitly enabled on a v4 provider connecting to a v3 server, the server will treat the composite routing key (documentName\0sessionId) as a literal document name, creating unintended documents. KeepsessionAwareness: false(the default) when connecting to a v3 server.
Highlights
Cross-Runtime Support
Hocuspocus is no longer tied to the Node.js ws library. The server now uses crossws, a universal WebSocket adapter, enabling Hocuspocus to run on:
- Node.js (with
wsoruWebSockets.js) - Bun
- Deno
- Cloudflare Workers
The built-in Server class continues to work as before for Node.js users. For other runtimes, use Hocuspocus directly with handleConnection(), which now accepts any WebSocketLike object and a web-standard Request.
Generic Context Type
All core classes and hook payloads now accept a generic Context type parameter, enabling end-to-end type safety:
interface MyContext {
userId: string;
permissions: string[];
}
const server = Server.configure<MyContext>({
async onAuthenticate({ context, token }) {
// context is typed as MyContext
return { userId: '123', permissions: ['read', 'write'] };
},
async onChange({ context }) {
// context.userId is typed as string
console.log(context.userId);
},
});
The generic defaults to any, so existing code without explicit typing continues to work.
Ordered Message Processing
Document updates are now processed sequentially in the order they are received. Previously, concurrent messages could be processed out of order if async hooks were involved. A new internal message queue ensures CRDT updates are applied consistently.
Web Standard Request/Headers
Hook payloads now use the web-standard Request and Headers objects instead of Node.js IncomingMessage and IncomingHttpHeaders. This aligns with the cross-runtime goal and provides a consistent API across all environments.
New Features
Server
- Cross-runtime WebSocket support via
crossws-- Bun, Deno, Cloudflare Workers, Node/uWebSockets all supported (non-breaking) - Generic
Contexttype parameter onHocuspocus,Server,Extension,Connection,ClientConnection,DirectConnection, and all hook payloads (non-breaking -- defaults toany) - Structured transaction origins -- new
TransactionOriginunion type (ConnectionTransactionOrigin | RedisTransactionOrigin | LocalTransactionOrigin) with helper functionsisTransactionOrigin()andshouldSkipStoreHooks()(breaking -- see upgrade guide) onLoadDocumentnow acceptsUint8Arrayreturns -- extensions can return raw Yjs updates instead of constructing a fullY.Doc, simplifying storage extensions (non-breaking)handleConnection()returnsClientConnection-- enables programmatic access to the connection lifecycle for custom integrations (non-breaking)- Ordered message processing -- messages are queued and processed sequentially per connection (non-breaking)
- Session awareness -- the server supports session-aware multiplexing, allowing multiple providers with the same document name on a single WebSocket. Each provider gets a unique
sessionIdrouted via a composite key. The server transparently falls back to plain document names for v3 providers (non-breaking) - Auth retry support -- failed authentication now properly cleans up state, allowing clients to retry without reconnecting (non-breaking)
- DirectConnection context --
openDirectConnection(documentName, context)now accepts and propagates a context object (non-breaking) - Store hooks on all changes --
onStoreDocumentis now triggered on any document change (not just WebSocket-originated ones), with explicit opt-out viaskipStoreHooksonLocalTransactionOrigin(non-breaking) - Provider version awareness -- the provider version is available on
Connection.providerVersionand in hook payloads (onConnect,onAuthenticate,connected), making it easier to introduce protocol changes in a backward-compatible way (non-breaking) SkipFurtherHooksError-- extensions can throwSkipFurtherHooksError(from@hocuspocus/common) inonStoreDocumentto signal that persistence was handled and remaining hooks should be skipped (non-breaking)
Provider
- Session awareness -- new
sessionAwarenessoption (default:false). When enabled, the provider embeds a uniquesessionIdin the document name field of every message, enabling multiple providers with the same document name on one WebSocket. Keep disabled when connecting to a v3 server (non-breaking) - Provider version sent during auth -- the provider now sends its package version to the server in the authentication message. The extra field is safely ignored by v3 servers (non-breaking)
- Awareness message deduplication -- when the WebSocket is not yet open and messages are queued, duplicate awareness messages for the same document are deduplicated, keeping only the latest one (non-breaking)
- Application-level Ping/Pong -- new
MessageType.Ping(9) andMessageType.Pong(10). The provider responds to server Ping messages with Pong, replacing WebSocket-level ping/pong which is not available in all runtimes. The provider works fine without receiving Pings (e.g., when connected to a v3 server) (non-breaking) wspackage types removed -- the provider no longer importsEvent,MessageEvent, orCloseEventfrom thewspackage. It uses web-standard types and types from@hocuspocus/commoninstead (breaking for TypeScript users who relied onwstypes being re-exported)CloseEventshape simplified -- theCloseEventpassed toonClosecallbacks no longer includestargetandtypefields. Onlycodeandreasonremain (breaking if youronClosehandler readsevent.targetorevent.type)- Attach collision detection --
HocuspocusProviderWebsocket.attach()now throws an error if you try to attach two authenticated providers with the same effective name. Previously it silently overwrote the existing provider (non-breaking for correct usage; may surface existing bugs) - Unknown message types handled gracefully -- unknown message types now log
console.errorinstead of throwing. This makes rolling out future protocol additions easier (non-breaking)
Bug Fixes
Server
- Auth state reset on failure -- when authentication fails, document state is cleaned up so the client can send a new auth message without reconnecting (#944) (non-breaking)
onLoadDocumentacceptsUint8Array-- the callback now correctly handles bothY.DocandUint8Arrayreturns (#795, #271) (non-breaking)- Close code type check -- close event codes are now properly validated as numbers (#1062) (non-breaking)
- Store hooks reliability --
onStoreDocumentnow triggers on any document change, preventing accidental data loss when updates lacked a Yjs origin (non-breaking) onStoreDocumentpayload type -- the Database extension and Logger extension now correctly type the parameter asonStoreDocumentPayloadinstead of the incorrectonChangePayload/onDisconnectPayload(non-breaking)- Document name validation -- empty and whitespace-only document names are now rejected on both WebSocket connections and direct connections (non-breaking)
- Store hook retry on failure -- when
onStoreDocumenthooks throw, the document stays in memory and retries are attempted to avoid data loss (non-breaking) - Graceful shutdown flushes pending stores --
Server.destroy()now immediately executes all pending debouncedonStoreDocumentcalls, ensuring documents are persisted before the server exits (even whenunloadImmediately: false) (non-breaking) - Memory optimization -- outgoing Yjs update messages are now encoded once and shared across connections instead of being re-created per connection (non-breaking)
Provider
- Unknown message types no longer crash the provider --
console.errorinstead ofthrow. This makes future protocol additions easier (non-breaking)
Infrastructure Changes
- Package manager: migrated from npm to pnpm workspaces
- Bundler: migrated from Rollup to Rolldown
- SQLite extension: migrated from
sqlite3to better-sqlite3 (synchronous API, actively maintained) - Node.js requirement:
>=22(specified in@hocuspocus/server) - Default timeout: increased from 30s to 60s
- Lerna: upgraded to v9 with pnpm as npm client
Upgrade Guide: v3 to v4
1. Update Dependencies
# Install v4
npm install @hocuspocus/server@^4.0.0 @hocuspocus/provider@^4.0.0
If you use the SQLite extension:
npm uninstall sqlite3
npm install @hocuspocus/extension-sqlite@^4.0.0 better-sqlite3
npm install -D @types/better-sqlite3
Node.js requirement: v4 requires Node.js 22 or later.
Server Changes
2. Request and Headers (Breaking)
All hook payloads now use web-standard Request and Headers 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;
}
Key differences:
requestHeaders['key']becomesrequestHeaders.get('key')request.socket.remoteAddressis no longer available -- usex-forwarded-fororx-real-ipheaders from your reverse proxyrequestis now a webRequestobject, not Node.jsIncomingMessage
Note: The onUpgrade and onRequest hooks still use Node.js IncomingMessage/ServerResponse since they operate at the HTTP level before the WebSocket upgrade.
3. onStoreDocument Payload (Breaking)
The onStoreDocument and afterStoreDocument payloads have been restructured. Several fields that were 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, // was: context
lastTransactionOrigin, // was: transactionOrigin
document,
documentName,
clientsCount,
instance,
// removed: requestHeaders, requestParameters, socketId
}) {
// ...
}
Migration:
context→lastContexttransactionOrigin→lastTransactionOriginrequestHeaders,requestParameters,socketId-- removed. If you need these, access them from thelastContext. Note that they contain the context of the last connection that triggered the hook.
4. onAwarenessUpdate Payload (Breaking)
The onAwarenessUpdate payload has been simplified. Connection-specific fields are removed and replaced with the source of the update.
Before (v3)
async onAwarenessUpdate({
context,
requestHeaders,
requestParameters,
socketId,
document,
documentName,
added, updated, removed, states,
}) {
// ...
}
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,
// removed: context, requestHeaders, requestParameters, socketId
}) {
// Access context through the connection if needed
const context = connection?.context;
}
5. WebSocket Type Changes (Breaking)
If your code references the WebSocket type from the ws package, update to WebSocketLike:
Before (v3)
import { WebSocket } from 'ws';
// In your code
const ws: WebSocket = connection.webSocket;
After (v4)
import type { WebSocketLike } from '@hocuspocus/server';
// In your code
const ws: WebSocketLike = connection.webSocket;
The WebSocketLike interface is minimal:
interface WebSocketLike {
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
close(code?: number, reason?: string): void;
readyState: number;
}
6. Server Constructor (Breaking)
WebSocket options are now passed inside the configuration object instead of as a separate parameter.
Before (v3)
import { Server } from '@hocuspocus/server';
const server = new Server(
{ port: 8080, extensions: [...] },
{ maxPayload: 1024 * 1024 } // ws options as 2nd arg
);
After (v4)
import { Server } from '@hocuspocus/server';
const server = new Server({
port: 8080,
extensions: [...],
websocketOptions: { maxPayload: 1024 * 1024 },
});
Server.configure() still works the same way -- just move websocketOptions into the config.
7. Transaction Origin Changes (Breaking)
If you inspect transactionOrigin in hooks (e.g., in onChange), the format has changed from raw values to structured objects.
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, shouldSkipStoreHooks } from '@hocuspocus/server';
async onChange({ transactionOrigin }) {
if (isTransactionOrigin(transactionOrigin)) {
switch (transactionOrigin.source) {
case 'redis':
// came from Redis
break;
case 'connection':
// came from a client connection
// transactionOrigin.connection is available
break;
case 'local':
// came from server-side code (e.g., DirectConnection)
break;
}
}
}
8. SQLite Extension (Breaking)
The SQLite extension has been migrated from the unmaintained sqlite3 package to better-sqlite3.
What you need to do:
- Replace the
sqlite3dependency withbetter-sqlite3 - No changes to your Hocuspocus configuration -- the extension API is the same
- Existing SQLite database files are fully compatible (no data migration needed)
If you wrote a custom schema for the SQLite extension, note that better-sqlite3 uses named parameters without the $ prefix:
$name→name$data→data
9. Custom handleConnection Integrations (Breaking)
If you call handleConnection() directly (e.g., for Express/Koa integration), the signature has changed:
Before (v3)
import { IncomingMessage } from 'http';
wss.on('connection', (ws: WebSocket, request: IncomingMessage) => {
hocuspocus.handleConnection(ws, request, context);
});
After (v4)
wss.on('connection', (ws, request: Request) => {
const clientConnection = hocuspocus.handleConnection(ws, request, context);
// clientConnection is now returned for programmatic access
});
Key changes:
requestmust be a web-standardRequest(notIncomingMessage)- The method now returns a
ClientConnectioninstance - The WebSocket no longer needs to be from the
wspackage -- anyWebSocketLikeworks - You are responsible for calling
clientConnection.handleMessage(data)andclientConnection.handleClose(event)if you're not using the built-inServerclass
10. Timeout Change (Non-Breaking)
The default connection timeout has increased from 30 seconds to 60 seconds. If you relied on the old default, you can restore it:
const server = Server.configure({
timeout: 30_000,
});
Provider Changes
11. CloseEvent Shape (Breaking)
The CloseEvent passed to onClose callbacks no longer includes target and type fields. Only code and reason remain.
Before (v3)
onClose({ event }) {
console.log(event.code, event.reason, event.target, event.type);
}
After (v4)
onClose({ event }) {
console.log(event.code, event.reason);
// event.target and event.type are no longer available
}
12. ws Package Types Removed (Breaking -- TypeScript Only)
The provider no longer imports or re-exports Event, MessageEvent, or CloseEvent from the ws package. If your TypeScript code relied on these types being available through the provider, import them from @hocuspocus/common or use web-standard types instead.
13. Session Awareness (Non-Breaking)
The provider has a new sessionAwareness option (default: false). When enabled, it embeds a unique sessionId in the document name field of every message, allowing multiple providers with the same document name on a single WebSocket.
No action is needed -- the default is false, which preserves v3 behavior. Enable it only when connecting to a v4 server and you need session multiplexing:
const provider = new HocuspocusProvider({
url: 'ws://localhost:1234',
name: 'my-document',
sessionAwareness: true, // opt-in for v4 server multiplexing
});
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 extension: 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 any TypeScript imports that relied on
wstypes from the provider - [ ] Test your extensions and hooks thoroughly
Breaking Changes
- Hook payloads now use web‑standard `Request` and `Headers`; code must replace `requestHeaders['key']` with `requestHeaders.get('key')` and drop `request.socket.remoteAddress`.
- `onStoreDocument` payload removed fields `context`, `requestHeaders`, `requestParameters`, `socketId`; renamed `context` → `lastContext` and `transactionOrigin` → `lastTransactionOrigin`.
- `onAwarenessUpdate` payload now provides structured `transactionOrigin`/`connection` instead of raw connection data; legacy fields removed.
- WebSocket type changed from `ws.WebSocket` to `WebSocketLike`; all imports must use the new interface definition.
- Server constructor no longer accepts a separate WebSocket options object; options must be placed in `config.websocketOptions`.
- `transactionOrigin` format replaced by structured `TransactionOrigin` objects with helper functions (`isTransactionOrigin`, `shouldSkipStoreHooks`).
- SQLite extension migrated from unmaintained `sqlite3` to actively maintained `better-sqlite3`; custom schema code must adjust named‑parameter syntax (e.g., `$name` → `name`).
- `handleConnection()` now returns a `ClientConnection` and expects a web‑standard `Request` instead of Node.js `IncomingMessage`.
- Provider `CloseEvent` no longer includes `target` or `type`; code reading those fields must be updated.
- `ws` package types (`Event`, `MessageEvent`, `CloseEvent`) are no longer re‑exported by the provider; TypeScript users must import from `@hocuspocus/common` or use web‑standard equivalents.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Hocuspocus 4
All releases →Beta — feedback welcome: [email protected]