Liveblocks vs PartyKit vs Hocuspocus: Real-Time Collaboration 2026
Liveblocks vs PartyKit vs Hocuspocus: Real-Time Collaboration Backends 2026
TL;DR
Building real-time collaborative features — multiplayer cursors, shared documents, live presence — requires infrastructure that most apps don't build themselves. Liveblocks is the complete managed collaboration platform: rooms, presence, LiveObject/LiveList data structures, comments, notifications, and AI copilot features — all as a managed service. PartyKit runs collaboration logic on Cloudflare Workers — you write the server logic, deploy globally, and it handles WebSocket connections at the edge. Hocuspocus is the Yjs-native WebSocket server — purpose-built for document collaboration (rich text editors, whiteboards) with complete self-hosting. For managed presence + cursors + comments: Liveblocks. For edge-deployed custom collaboration logic: PartyKit. For Yjs-powered document sync, self-hosted: Hocuspocus.
Key Takeaways
- Liveblocks has the richest feature set — presence, storage, comments, notifications, AI integration, all pre-built
- PartyKit runs on Cloudflare's edge — 300+ global locations, WebSocket connections are geographically close to users
- Hocuspocus is the reference Yjs server — used in production by Tiptap's hosted collaboration
- Yjs is the CRDT foundation — Liveblocks and Hocuspocus use it; PartyKit lets you bring your own CRDT
- Liveblocks free tier: 50 MAU — generous for prototyping; pricing scales with active users
- PartyKit is now part of Cloudflare — officially acquired in 2024, deeply integrated
- All three have React hooks —
useOthers(),useRoom(), and mutation helpers
Why Collaboration Infrastructure is Hard
Real-time collaboration requires more than WebSockets:
- CRDT merge conflict resolution — two users typing simultaneously must produce consistent result
- Presence — showing who's in the room, where their cursor is, what they're editing
- Persistence — reconnecting users get the current state, not a blank slate
- Global latency — 200ms input latency makes collaborative editing feel broken
- Connection management — reconnects, offline queuing, connection state UI
Liveblocks: The Managed Collaboration Platform
Liveblocks provides the complete stack: infrastructure, data structures, React hooks, and pre-built UI components. You focus on your product; they handle the collaboration layer.
Installation
npm install @liveblocks/client @liveblocks/react
Room Setup
// liveblocks.config.ts — define types for your collaboration data
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";
const client = createClient({
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
// Or for authenticated access:
authEndpoint: "/api/liveblocks-auth",
});
// Define your collaborative data structures
type Presence = {
cursor: { x: number; y: number } | null;
name: string;
color: string;
};
type Storage = {
canvasObjects: LiveList<CanvasObject>;
documentTitle: LiveObject<{ title: string }>;
};
type UserMeta = {
id: string;
info: { name: string; avatar: string };
};
export const {
RoomProvider,
useOthers,
useMyPresence,
useSelf,
useStorage,
useMutation,
useRoom,
} = createRoomContext<Presence, Storage, UserMeta>(client);
Presence and Cursors
// components/CollaborativeCanvas.tsx
import { useCallback, useEffect } from "react";
import { RoomProvider, useOthers, useMyPresence } from "@/liveblocks.config";
function Cursors() {
const others = useOthers();
return (
<>
{others.map(({ connectionId, presence }) => {
if (!presence.cursor) return null;
return (
<div
key={connectionId}
style={{
position: "absolute",
left: presence.cursor.x,
top: presence.cursor.y,
pointerEvents: "none",
transform: "translate(-4px, -4px)",
}}
>
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill={presence.color}
d="M5.65 1.27L5.26.86 0 15.18l4.6-1.56 2.94 5.06 1.73-1 -2.94-5.06 4.6-1.56L5.65 1.27z"
/>
</svg>
<span
style={{
background: presence.color,
color: "white",
borderRadius: "4px",
padding: "2px 6px",
fontSize: "12px",
marginTop: "2px",
}}
>
{presence.name}
</span>
</div>
);
})}
</>
);
}
function Canvas() {
const [_, updateMyPresence] = useMyPresence();
const handleMouseMove = useCallback(
(event: React.MouseEvent) => {
const rect = event.currentTarget.getBoundingClientRect();
updateMyPresence({
cursor: {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
},
});
},
[updateMyPresence]
);
return (
<div
className="relative w-full h-[600px] bg-gray-50"
onMouseMove={handleMouseMove}
onMouseLeave={() => updateMyPresence({ cursor: null })}
>
<Cursors />
{/* Your canvas content */}
</div>
);
}
export function CollaborativeCanvas({ roomId }: { roomId: string }) {
return (
<RoomProvider
id={roomId}
initialPresence={{ cursor: null, name: "Anonymous", color: "#3b82f6" }}
>
<Canvas />
</RoomProvider>
);
}
LiveStorage (Shared Data Structures)
import { LiveList, LiveObject } from "@liveblocks/client";
import { useStorage, useMutation } from "@/liveblocks.config";
// Shared todo list — all users see updates in real-time
function CollaborativeTodos() {
const todos = useStorage((root) => root.todos);
const addTodo = useMutation(({ storage }, text: string) => {
const todos = storage.get("todos");
todos.push({ id: Date.now(), text, completed: false });
}, []);
const toggleTodo = useMutation(({ storage }, id: number) => {
const todos = storage.get("todos");
const index = todos.findIndex((t) => t.id === id);
if (index !== -1) {
const todo = todos.get(index);
todos.set(index, { ...todo, completed: !todo.completed });
}
}, []);
return (
<div>
{todos?.map((todo) => (
<div key={todo.id} onClick={() => toggleTodo(todo.id)}>
<input type="checkbox" checked={todo.completed} readOnly />
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
</span>
</div>
))}
<button onClick={() => addTodo("New task")}>Add Task</button>
</div>
);
}
PartyKit: Edge-Deployed Collaboration
PartyKit runs your WebSocket server logic on Cloudflare Workers globally. You write a JavaScript class, deploy it, and connect from any client.
Installation
npm install partykit partysocket
npx partykit init my-collab-app
PartyKit Server
// party/index.ts — runs on Cloudflare's edge
import type * as Party from "partykit/server";
type Connection = Party.Connection<{ user: string; color: string }>;
export default class CollabRoom implements Party.Server {
constructor(readonly room: Party.Room) {}
// Track connected users
private users = new Map<string, { name: string; cursor: { x: number; y: number } | null }>();
onConnect(conn: Connection, ctx: Party.ConnectionContext) {
const url = new URL(ctx.request.url);
const name = url.searchParams.get("name") ?? "Anonymous";
this.users.set(conn.id, { name, cursor: null });
// Send current state to the new connection
conn.send(JSON.stringify({
type: "init",
users: Object.fromEntries(this.users),
}));
// Notify others
this.room.broadcast(
JSON.stringify({ type: "user_joined", id: conn.id, name }),
[conn.id] // Exclude the new user
);
}
onMessage(message: string, sender: Connection) {
const data = JSON.parse(message);
if (data.type === "cursor_move") {
const user = this.users.get(sender.id);
if (user) {
user.cursor = data.cursor;
// Broadcast cursor position to all other connections
this.room.broadcast(
JSON.stringify({
type: "cursor_update",
id: sender.id,
cursor: data.cursor,
name: user.name,
}),
[sender.id]
);
}
}
if (data.type === "draw") {
// Broadcast drawing operations to all connections
this.room.broadcast(message, [sender.id]);
}
}
onClose(conn: Connection) {
this.users.delete(conn.id);
this.room.broadcast(
JSON.stringify({ type: "user_left", id: conn.id })
);
}
}
PartyKit Client (React)
import { useEffect, useRef, useState } from "react";
import PartySocket from "partysocket";
function usePartySocket(roomId: string, userName: string) {
const [users, setUsers] = useState<Record<string, any>>({});
const socketRef = useRef<PartySocket | null>(null);
useEffect(() => {
const socket = new PartySocket({
host: process.env.NEXT_PUBLIC_PARTYKIT_HOST!, // your-app.partykit.dev
room: roomId,
query: { name: userName },
});
socketRef.current = socket;
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
if (data.type === "init") setUsers(data.users);
if (data.type === "user_joined") {
setUsers((prev) => ({ ...prev, [data.id]: { name: data.name, cursor: null } }));
}
if (data.type === "cursor_update") {
setUsers((prev) => ({
...prev,
[data.id]: { ...prev[data.id], cursor: data.cursor },
}));
}
if (data.type === "user_left") {
setUsers((prev) => {
const next = { ...prev };
delete next[data.id];
return next;
});
}
});
return () => socket.close();
}, [roomId, userName]);
const sendCursorMove = (x: number, y: number) => {
socketRef.current?.send(JSON.stringify({
type: "cursor_move",
cursor: { x, y },
}));
};
return { users, sendCursorMove };
}
Hocuspocus: Yjs-Native Document Sync
Hocuspocus is the WebSocket server designed specifically for Yjs — the CRDT library used by TipTap, BlockNotes, and other collaborative editors. Self-host it and get document sync, persistence, and authentication.
Installation
npm install @hocuspocus/server @hocuspocus/extension-database
Hocuspocus Server Setup
// server.ts — Hocuspocus with database persistence
import { Server } from "@hocuspocus/server";
import { Database } from "@hocuspocus/extension-database";
import { Logger } from "@hocuspocus/extension-logger";
import * as Y from "yjs";
const server = Server.configure({
port: 1234,
extensions: [
new Logger(),
new Database({
// Load document from database
fetch: async ({ documentName }) => {
const doc = await db.documents.findUnique({
where: { name: documentName },
});
return doc?.content ?? null; // Uint8Array or null
},
// Store document to database
store: async ({ documentName, state }) => {
await db.documents.upsert({
where: { name: documentName },
create: { name: documentName, content: state },
update: { content: state },
});
},
}),
],
// Authentication
async onAuthenticate(data) {
const { token } = data;
const user = await verifyToken(token);
if (!user) {
throw new Error("Unauthorized");
}
// Pass user data to be available in other hooks
return { user };
},
// Authorization per document
async onLoadDocument(data) {
const { documentName, context } = data;
const hasAccess = await checkAccess(context.user.id, documentName);
if (!hasAccess) {
throw new Error("Forbidden");
}
},
});
server.listen();
React Integration with TipTap
// CollaborativeEditor.tsx — TipTap + Hocuspocus
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
import { useEffect, useState } from "react";
const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({
url: "ws://localhost:1234",
name: "document:123",
document: ydoc,
token: userAuthToken,
onConnect: () => console.log("Connected"),
onDisconnect: () => console.log("Disconnected"),
onSynced: () => console.log("Synced with server"),
});
export function CollaborativeEditor() {
const [synced, setSynced] = useState(false);
const editor = useEditor({
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({ document: ydoc }),
CollaborationCursor.configure({
provider,
user: { name: currentUser.name, color: currentUser.color },
}),
],
});
useEffect(() => {
provider.on("synced", () => setSynced(true));
return () => provider.disconnect();
}, []);
if (!synced) return <div>Connecting...</div>;
return (
<div>
<EditorContent editor={editor} />
</div>
);
}
Feature Comparison
| Feature | Liveblocks | PartyKit | Hocuspocus |
|---|---|---|---|
| Hosting | Managed SaaS | Cloudflare Edge | Self-hosted |
| Yjs support | ✅ | Bring your own | ✅ Native |
| Presence/cursors | ✅ Built-in | Custom code | Via Y.Awareness |
| Comments system | ✅ Pre-built | Custom code | ❌ |
| Notifications | ✅ Pre-built | Custom code | ❌ |
| Data persistence | ✅ Automatic | Manual | ✅ Extensions |
| Global edge | Via CDN | ✅ 300+ PoPs | Self-deploy |
| Authentication | ✅ | Custom | ✅ |
| React hooks | ✅ Full suite | Manual | Via HocuspocusProvider |
| Open source | ❌ | ✅ | ✅ MIT |
| Self-hostable | ❌ | ✅ (limited) | ✅ Full |
| GitHub stars | ~5k | ~4k | ~3k |
| Free tier | 50 MAU | Pay per request | Self-hosted |
| Rich text support | Via Yjs | Custom | ✅ Native (TipTap) |
When to Use Each
Choose Liveblocks if:
- You want presence, cursors, shared data, comments, and notifications without building backend logic
- Managed infrastructure with 99.99% uptime SLA is required
- You need AI copilot features (Liveblocks AI extension)
- Time-to-market matters more than cost at scale
Choose PartyKit if:
- Your collaboration logic doesn't fit standard CRDT patterns (games, custom protocols)
- Global edge latency is critical (Cloudflare's 300+ edge locations)
- You want to own and customize the server logic completely
- Cost predictability via Cloudflare's per-request pricing model
Choose Hocuspocus if:
- You're building a rich text editor or document editor with TipTap/ProseMirror
- Self-hosting is required for compliance or data residency
- You want to integrate with your existing WebSocket infrastructure
- The Yjs ecosystem (whiteboards, code editors, spreadsheets) is your primary use case
Methodology
Data sourced from GitHub repositories (star counts as of February 2026), official documentation, npm download statistics (January 2026), and community benchmarks from builder communities on Discord. Pricing data verified from official pricing pages. Feature availability verified against documentation.
Related: Socket.io vs WebSockets vs SSE for real-time infrastructure options, or Supabase vs Firebase vs Appwrite for real-time database solutions.