Skip to main content
Hocuspocus is TipTap’s open-source Yjs WebSocket server. It’s a mature, battle-tested option for self-hosted collaboration.

Setup

Server

npm install @hocuspocus/server
import { Server } from "@hocuspocus/server";

const server = Server.configure({
  port: 1234,

  async onLoadDocument(data) {
    // Load document from database
    const state = await db.getDocument(data.documentName);
    if (state) {
      Y.applyUpdate(data.document, state);
    }
    return data.document;
  },

  async onStoreDocument(data) {
    // Save document to database
    const state = Y.encodeStateAsUpdate(data.document);
    await db.saveDocument(data.documentName, state);
  },

  async onAuthenticate(data) {
    // Validate token
    const user = await validateToken(data.token);
    if (!user) {
      throw new Error("Unauthorized");
    }
    return { user };
  },
});

server.listen();

Client

Use the provider-agnostic API to connect SuperDoc:
npm install @hocuspocus/provider yjs
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
import { SuperDoc } from "superdoc";

const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({
  url: "ws://localhost:1234",
  name: "document-123",
  document: ydoc,
  token: "auth-token", // Optional
});

// Wait for sync before creating editor
provider.on("synced", () => {
  const superdoc = new SuperDoc({
    selector: "#editor",
    documentMode: "editing",
    user: {
      name: "John Smith",
      email: "[email protected]",
    },
    modules: {
      collaboration: { ydoc, provider },
    },
  });
});

React Example

import { useEffect, useRef, useState } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
import { SuperDoc } from "superdoc";
import "superdoc/style.css";

export default function Editor() {
  const superdocRef = useRef<SuperDoc | null>(null);
  const [users, setUsers] = useState<any[]>([]);

  useEffect(() => {
    const ydoc = new Y.Doc();
    const provider = new HocuspocusProvider({
      url: "ws://localhost:1234",
      name: "my-document",
      document: ydoc,
    });

    provider.on("synced", () => {
      superdocRef.current = new SuperDoc({
        selector: "#superdoc",
        documentMode: "editing",
        user: {
          name: `User ${Math.floor(Math.random() * 1000)}`,
          email: "[email protected]",
        },
        modules: {
          collaboration: { ydoc, provider },
        },
        onAwarenessUpdate: ({ states }) => {
          setUsers(states.filter((s) => s.user));
        },
      });
    });

    return () => {
      superdocRef.current?.destroy();
      provider.destroy();
    };
  }, []);

  return (
    <div>
      <div className="users">
        {users.map((u, i) => (
          <span key={i} style={{ background: u.user?.color }}>
            {u.user?.name}
          </span>
        ))}
      </div>
      <div id="superdoc" style={{ height: "100vh" }} />
    </div>
  );
}

Server Configuration

Basic Options

Server.configure({
  port: 1234,
  timeout: 30000, // Connection timeout
  debounce: 2000, // Debounce document saves
  maxDebounce: 10000, // Max wait before save
  quiet: false, // Disable logging
});

Hooks

HookPurpose
onLoadDocumentLoad document from storage
onStoreDocumentSave document to storage
onAuthenticateValidate user tokens
onChangeReact to document changes
onConnectHandle new connections
onDisconnectHandle disconnections

Persistence Example

import { Server } from "@hocuspocus/server";
import { Database } from "@hocuspocus/extension-database";

const server = Server.configure({
  extensions: [
    new Database({
      fetch: async ({ documentName }) => {
        const doc = await db.findOne({ name: documentName });
        return doc?.data || null;
      },
      store: async ({ documentName, state }) => {
        await db.upsert({ name: documentName }, { data: state });
      },
    }),
  ],
});

Provider Options

const provider = new HocuspocusProvider({
  url: "ws://localhost:1234",
  name: "document-id",
  document: ydoc,

  // Optional
  token: "auth-token",
  awareness: awareness, // Custom awareness instance
  connect: true, // Auto-connect on create
  preserveConnection: true, // Keep connection on destroy
  broadcast: true, // Broadcast changes to tabs
});

Events

Provider Events

// Sync status
provider.on("synced", () => {
  console.log("Document synced");
});

// Connection status
provider.on("status", ({ status }) => {
  // 'connecting' | 'connected' | 'disconnected'
});

// Authentication
provider.on("authenticationFailed", ({ reason }) => {
  console.error("Auth failed:", reason);
});

Production Deployment

Docker

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --production

COPY . .

EXPOSE 1234
CMD ["node", "server.js"]

With Redis (Scaling)

import { Server } from "@hocuspocus/server";
import { Redis } from "@hocuspocus/extension-redis";

Server.configure({
  extensions: [
    new Redis({
      host: "localhost",
      port: 6379,
    }),
  ],
});

Resources

Next Steps