Build real-time collaborative applications with Hocuspocus 4 and Yjs CRDT. From WebSocket server setup to production deployment — a practical guide with code examples for Node, Bun, Deno, and Cloudflare Workers.
Real-time collaboration is no longer a nice-to-have — it's expected. Google Docs, Notion, Figma, and Linear have set the bar: multiple users should be able to edit the same document simultaneously, see each other's cursors, and never lose data — even when the network drops.
Building this from scratch is notoriously hard. You need conflict resolution, operational transforms, state synchronization, presence awareness, and persistence. That's where Yjs and Hocuspocus come in.
Yjs is a CRDT (Conflict-Free Replicated Data Type) library that handles conflict resolution automatically. Hocuspocus is the WebSocket-based backend server that manages Yjs document synchronization across clients, with built-in persistence, authentication, and scaling.
Hocuspocus 4 was released in June 2026 and is the most significant update yet — adding multi-platform support (Node.js, Bun, Deno, Cloudflare Workers), a redesigned extension API, improved persistence, and significant performance improvements. Let's dive deep.
Before diving into Hocuspocus, it's important to understand the technology underneath. CRDTs (Conflict-Free Replicated Data Types) are data structures that can be replicated across multiple users and merged automatically without conflicts — no central coordination required.
Unlike Operational Transformation (used by Google Docs), CRDTs have several key advantages:
Yjs implements the YATA algorithm, a type of CRDT optimized for text editing. It represents the document as a linked list of items, each with a unique identifier (client ID + clock). When two users insert text at the same position, Yjs orders the insertions deterministically by their identifiers — ensuring all clients converge to the same state.
// Yjs YATA algorithm — simplified concept
// Each item has: { id: [clientId, clock], content, origin }
// Two inserts at position 5 from different clients:
const itemA = { id: [1, 42], content: "Hello", origin: [null, 5] };
const itemB = { id: [2, 17], content: "World", origin: [null, 5] };
// Yjs orders by (clientId, clock) — itemB comes before itemA
// because clientId 2 < clientId 1 creates a deterministic ordering
// Integration: "WorldHello" — same on every client
Hocuspocus is the official server-side solution for Yjs. Think of it as the "backend for Yjs" — it handles:
Without Hocuspocus, you'd need to build all of this yourself on top of raw Yjs — which is possible, but time-consuming and error-prone. Hocuspocus packages it into a clean, configurable server with a plugin architecture.
Hocuspocus 4, released in early June 2026, is a ground-up rewrite with several major improvements:
The biggest headline: Hocuspocus 4 runs on Node.js, Bun, Deno, and Cloudflare Workers. The same API works across all platforms, with platform-specific adapters for differences in file I/O, WebSocket implementations, and storage backends.
The extension system has been overhauled. Instead of the old class-based hooks, Hocuspocus 4 uses a middleware-inspired pipeline where extensions register handlers for lifecycle events:
beforeConnect — validate connection requests before they're acceptedonConnect — set up per-connection state (user context, permissions)onLoadDocument — transform or hydrate documents when they're loadedonStoreDocument — intercept and modify documents before persistenceonDisconnect — cleanup when a client leavesSQLite persistence now uses WAL mode by default, dramatically improving concurrent read/write performance. The new PostgreSQL adapter supports connection pooling and is suitable for multi-server deployments. The Redis adapter enables ephemeral document caching with optional persistence to disk.
Hocuspocus 4 provides a first-class Cloudflare Workers adapter that uses Durable Objects for stateful WebSocket connections and DO storage for document persistence. This means you can run your collaboration infrastructure entirely at the edge with global low-latency access.
The new delta-sync protocol reduces bandwidth by 40-60% for typical editing sessions. Memory management has been rewritten to handle documents with thousands of concurrent editors more efficiently. The server startup time is roughly 3x faster than Hocuspocus 3.
Let's build a minimal Hocuspocus 4 server. First, install the package:
npm install @hocuspocus/server @hocuspocus/extension-sqlite
Now create a basic server:
import { Server } from '@hocuspocus/server'
import { SQLiteExtension } from '@hocuspocus/extension-sqlite'
const server = Server.configure({
port: 8080,
// Persist documents to SQLite
extensions: [
new SQLiteExtension({
database: 'collab.db',
// WAL mode enabled by default in Hocuspocus 4
}),
],
// Authentication hook
async beforeConnect({ connection }) {
const token = connection.token
if (!token || !validateToken(token)) {
throw new Error('Authentication required')
}
const userData = decodeToken(token)
return { user: userData }
},
})
server.listen().then(() => {
console.log('Hocuspocus 4 server running on port 8080')
})
That's it. With about 15 lines of server code, you have:
On the client side, you connect to the Hocuspocus server using the
@hocuspocus/provider package:
npm install @hocuspocus/provider yjs
import * as Y from 'yjs'
import { HocuspocusProvider } from '@hocuspocus/provider'
// Create a Yjs document
const doc = new Y.Doc()
// Connect to Hocuspocus server
const provider = new HocuspocusProvider({
url: 'ws://localhost:8080',
name: 'my-document-id', // unique document identifier
doc,
token: 'your-jwt-token', // passed to beforeConnect hook
})
// The document is now synced with the server
// All changes are automatically broadcast to other clients
// Example: observe changes
const array = doc.getArray('my-list')
array.observe(event => {
console.log('Document changed:', array.toArray())
})
// Make a change — it's automatically synced
array.push(['Hello from Client A!'])
The provider automatically handles reconnection, state synchronization, and conflict resolution. You can subscribe to connection status events:
provider.on('status', (event) => {
switch (event.status) {
case 'connected':
console.log('Connected to Hocuspocus')
break
case 'disconnected':
console.log('Disconnected — reconnecting...')
break
case 'connecting':
console.log('Attempting to connect...')
break
}
})
The most common use case for Yjs is collaborative rich text editing. The y-prosemirror binding connects Yjs documents to ProseMirror, one of the most powerful rich text editors in the JavaScript ecosystem:
npm install y-prosemirror prosemirror-state prosemirror-view prosemirror-model prosemirror-schema-basic prosemirror-example-setup
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
import { yDocPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
import * as Y from 'yjs'
import { HocuspocusProvider } from '@hocuspocus/provider'
const doc = new Y.Doc()
const provider = new HocuspocusProvider({
url: 'ws://localhost:8080',
name: 'document-id',
doc,
})
// Get the Yjs type for ProseMirror
const yDocType = doc.get('prosemirror', Y.XmlFragment)
// Create the ProseMirror editor connected to Yjs
const state = EditorState.create({
schema,
plugins: [
yDocPlugin(yDocType), // bidirectional sync with Yjs
yCursorPlugin(provider.awareness!), // show other users' cursors
yUndoPlugin(), // shared undo history
...exampleSetup({ schema }),
],
})
const view = new EditorView(document.querySelector('#editor'), { state })
// Now every keystroke is synced in real-time
// Other users see your cursor position and edits live
The yCursorPlugin provides collaborative cursors and selections
out of the box — users see each other's cursor positions, selections, and
even user names, colors, and avatars.
Hocuspocus 4 supports four persistence backends. Choosing the right one depends on your deployment model and scale requirements:
| Backend | Best For | Scaling | Performance |
|---|---|---|---|
| SQLite | Single-server, prototyping, small teams | Single instance | Fast with WAL mode; ~10K docs on modest hardware |
| PostgreSQL | Multi-server, production deployments | Horizontal via connection pooling | Good; write throughput limited by row locking |
| Redis | High-throughput, ephemeral docs | Cluster mode supported | Excellent; in-memory speed |
| Cloudflare DO | Edge deployments, global audiences | Automatic (Durable Objects) | Very good; data stays near users |
import { Server } from '@hocuspocus/server'
import { PostgreSQL } from '@hocuspocus/extension-postgresql'
const server = Server.configure({
port: 8080,
extensions: [
new PostgreSQL({
connection: {
host: 'localhost',
port: 5432,
database: 'collab',
user: 'app',
password: process.env.DB_PASSWORD,
},
// Auto-migrate schema on first run
migrate: true,
// Connection pool of 10 connections
poolSize: 10,
}),
],
})
server.listen()
Authentication in Hocuspocus 4 works through the extension pipeline. The
beforeConnect hook fires before the WebSocket connection is
established, allowing you to validate credentials and reject unauthorized
clients:
import { Server } from '@hocuspocus/server'
import jwt from 'jsonwebtoken'
const server = Server.configure({
// ...other config...
async beforeConnect({ connection }) {
// Extract JWT from the connection token
const { token } = connection
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
// Return user context — available in all subsequent hooks
return {
user: {
id: decoded.sub,
name: decoded.name,
role: decoded.role,
},
}
} catch (error) {
// Reject the connection
throw new Error('Invalid or expired token')
}
},
// Per-document authorization
async onLoadDocument({ documentName, user }) {
// Check if user has access to this document
const hasAccess = await checkDocumentAccess(user.id, documentName)
if (!hasAccess) {
throw new Error('Access denied to this document')
}
},
})
For fine-grained permissions, you can implement per-document access control, read-only access for viewers, and role-based limitations on editing:
async onStoreDocument({ documentName, document, user }) {
// Only admins can delete document history
if (document.meta?.requestedDelete && user.role !== 'admin') {
throw new Error('Only admins can delete documents')
}
// Log who saved what
console.log(`User ${user.id} saved changes to ${documentName}`)
// Optionally transform the document before saving
// (e.g., sanitize HTML in rich text content)
return document
}
Hocuspocus 4's Cloudflare Workers support is one of its most exciting features. You can deploy the entire collaboration backend to the edge, with documents stored in Durable Objects:
import { Server } from '@hocuspocus/server'
import { CloudflareDO } from '@hocuspocus/extension-cloudflare'
// This is a Durable Object that runs on Cloudflare's edge
export class HocuspocusDO {
constructor(state, env) {
this.server = Server.configure({
extensions: [
new CloudflareDO({
state,
// Durable Objects storage for persistence
storage: state.storage,
}),
],
})
}
async fetch(request) {
return await this.server.fetch(request)
}
}
// wrangler.toml
// [durable_objects]
// bindings = [{ name = "HOCUSPOCUS", class_name = "HocuspocusDO" }]
This deploys to Cloudflare's global network — users connect to the nearest data center, and Durable Objects ensure state is consistent across requests. No servers to manage, no regions to configure.
When your application grows beyond a single server, Hocuspocus 4 supports horizontal scaling through its Redis-based pub/sub layer:
import { Server } from '@hocuspocus/server'
import { RedisExtension } from '@hocuspocus/extension-redis'
const server = Server.configure({
port: 8080,
extensions: [
new RedisExtension({
// Redis pub/sub for cross-server sync
publisher: { host: 'redis-cluster', port: 6379 },
subscriber: { host: 'redis-cluster', port: 6379 },
}),
// Persistence on each server
new PostgreSQL({ /* ... */ }),
],
})
// Deploy behind a load balancer
// All instances share document state via Redis pub/sub
// Each instance persists independently to PostgreSQL
With this setup, you can run multiple Hocuspocus instances behind a load balancer. When user A connects to instance 1 and user B connects to instance 2, Redis pub/sub ensures both receive each other's changes. PostgreSQL provides durable persistence that survives instance restarts.
Beyond document sync, collaborative apps need awareness —
knowing who's online, where their cursor is, and what they're doing. Yjs
provides the Awareness protocol, and Hocuspocus relays it
between clients:
// Client: update awareness state
provider.awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff6b6b',
avatar: '/avatars/alice.png',
},
cursor: { x: 450, y: 120 },
selection: { from: 23, to: 45 },
})
// Client: listen for other users' awareness
provider.awareness.on('change', ({ states }) => {
states.forEach((state, clientId) => {
if (clientId !== doc.clientID) {
// Render other users' cursors and selections
renderRemoteCursor(state.user, state.cursor)
}
})
})
This pattern enables features like:
| Feature | Hocuspocus 4 | Liveblocks | PartyKit | ShareDB |
|---|---|---|---|---|
| Technology | Yjs CRDT | Proprietary CRDT | Yjs CRDT | OT (JSON0) |
| Self-Hosted | Yes | No (SaaS) | Partially | Yes |
| Platform | Node, Bun, Deno, CF Workers | Node (via SDK) | PartyKit Cloud | Node.js |
| Persistence | SQLite, PostgreSQL, Redis, DO | Managed | SQLite, DO | MongoDB, PostgreSQL |
| Offline Support | Built-in (CRDT) | Limited | Built-in (CRDT) | No |
| Rich Text | ProseMirror, Quill, TipTap | Built-in editor | ProseMirror | Quill (via plugin) |
| Pricing | Free (MIT) | From $599/mo | From $49/mo | Free (MIT) |
Hocuspocus stands out for its open-source nature, multi-platform support, and flexible persistence. You own your infrastructure, your data never touches a third-party service, and you can deploy anywhere from a $5 VPS to Cloudflare's global network.
Hocuspocus and Yjs power collaborative features in production at scale:
The classic use case. Multiple users edit the same document, see each other's cursors, and never lose work. Companies use it for internal wikis, proposal writing, code documentation, and meeting notes. With TipTap or ProseMirror as the editor, you get a full-featured rich text experience.
Yjs isn't limited to text. With y-indexeddb for offline caching
and custom Yjs types for shapes, connectors, and layers, teams can collaborate
on diagrams, wireframes, and whiteboards in real time — similar to Figma or
Excalidraw.
Using y-monaco (Yjs binding for Monaco Editor) or
y-codemirror.next, you can add real-time collaborative editing
to code editors. Teams use this for pair programming, code reviews, and
classroom environments. Hocuspocus handles document persistence so code
sessions survive server restarts.
Task boards, kanban boards, and databases benefit from Yjs's fine-grained
synchronization. Each card, comment, and status change syncs in real time
without page refreshes. Yjs's Y.Map and Y.Array
types map naturally to document-oriented data models.
If you're upgrading from Hocuspocus 3, the migration path is straightforward but requires attention to a few breaking changes:
/ to /hocuspocus; configure webSocketPath if legacy compatibility is needed
The Hocuspocus team provides a @hocuspocus/migration package that
automates most of the migration. Test thoroughly on a staging environment before
upgrading production.
Need real-time collaboration? See my development services
npm install @hocuspocus/server @hocuspocus/extension-sqlite. Create a server with Server.configure(), add extensions for persistence and authentication, and call server.listen(). A minimal setup requires about 15 lines of code. Hocuspocus handles WebSocket connections, document sync, and conflict resolution automatically. It runs on Node.js, Bun, Deno, or Cloudflare Workers.@hocuspocus/extension-cloudflare adapter.beforeConnect hook fires before the WebSocket connection is established — you validate JWT tokens, API keys, or any credential format. Return user context (id, role, permissions) which is available in subsequent hooks like onLoadDocument for per-document authorization. Invalid tokens reject the connection with an error.@hocuspocus/provider package combined with Yjs document types. For rich text editing, pair it with y-prosemirror (React ProseMirror) or y-quill (React Quill). React components subscribe to Yjs document changes via Y.Doc observers and update automatically.Hocuspocus 4 makes real-time collaboration accessible to any project. Whether you're building the next Google Docs, a collaborative whiteboard, a pair programming tool, or a real-time project management app — the combination of Yjs CRDT and Hocuspocus gives you a production-ready foundation.
The best part? It's free, open-source, and self-hostable. You own your data and your infrastructure. No per-seat licensing, no vendor lock-in, no data leaving your servers.
If you're planning a project that needs real-time collaboration and want an experienced developer to architect and build it, let's talk. I've built collaborative applications with Yjs, Hocuspocus, WebSockets, and various frontend frameworks — I can help you choose the right architecture and avoid the common pitfalls.
Need real-time collaboration for your app? I can help architect and build it. Tell me about your project — free initial consultation.