Skip to main content

Liveblocks vs PartyKit vs Hocuspocus: Real-Time Collaboration 2026

·PkgPulse Team

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 hooksuseOthers(), 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

FeatureLiveblocksPartyKitHocuspocus
HostingManaged SaaSCloudflare EdgeSelf-hosted
Yjs supportBring your own✅ Native
Presence/cursors✅ Built-inCustom codeVia Y.Awareness
Comments system✅ Pre-builtCustom code
Notifications✅ Pre-builtCustom code
Data persistence✅ AutomaticManual✅ Extensions
Global edgeVia CDN✅ 300+ PoPsSelf-deploy
AuthenticationCustom
React hooks✅ Full suiteManualVia HocuspocusProvider
Open source✅ MIT
Self-hostable✅ (limited)✅ Full
GitHub stars~5k~4k~3k
Free tier50 MAUPay per requestSelf-hosted
Rich text supportVia YjsCustom✅ 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.