Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/partykit/llms.txt

Use this file to discover all available pages before exploring further.

y-partyserver is a PartyServer addon that hosts a Yjs CRDT document inside a Durable Object. Yjs handles the hard parts of collaborative editing — conflict resolution, offline changes, awareness — while PartyServer provides the WebSocket infrastructure and persistence hooks. The result is a collaborative text editor backend you can deploy to Cloudflare Workers in minutes.

Installation

npm install partyserver y-partyserver yjs
You’ll also need a Yjs-compatible editor binding for the frontend — for example @tiptap/extension-collaboration, y-monaco, or y-lexical.

Server setup

Minimal server

The simplest backend re-exports YServer directly as your Durable Object class:
// src/server.ts
import { routePartykitRequest } from "partyserver";
import { YServer } from "y-partyserver";
import { env } from "cloudflare:workers";

// Re-export YServer under your own name
export { YServer as Document };

export default {
  async fetch(request: Request): Promise<Response> {
    return (
      (await routePartykitRequest(request, env)) ||
      new Response("Not Found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;
// wrangler.jsonc
{
  "name": "my-editor",
  "main": "src/server.ts",
  "durable_objects": {
    "bindings": [
      { "name": "Document", "class_name": "Document" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Document"] }
  ]
}

Server with persistence

By default, the Yjs document lives in memory and is lost if all clients disconnect. Override onLoad and onSave to persist it between sessions.
// src/server.ts
import { routePartykitRequest } from "partyserver";
import { YServer } from "y-partyserver";
import type { CallbackOptions } from "y-partyserver";
import * as Y from "yjs";
import { env } from "cloudflare:workers";

export class Document extends YServer {
  // Controls how often onSave is called after edits
  static callbackOptions: CallbackOptions = {
    debounceWait: 2000,      // wait 2 s after last edit before saving
    debounceMaxWait: 10000,  // save at least every 10 s during active editing
    timeout: 5000,
  };

  static options = { hibernate: true };

  async onStart() {
    // Create the storage table if it doesn't exist
    this.ctx.storage.sql.exec(
      "CREATE TABLE IF NOT EXISTS documents (id TEXT PRIMARY KEY, content BLOB)"
    );
    return super.onStart();
  }

  async onLoad() {
    // Called once when the first client connects — load the persisted document
    const [row] = [
      ...this.ctx.storage.sql.exec(
        "SELECT content FROM documents WHERE id = ? LIMIT 1",
        this.name
      ),
    ];
    if (row) {
      Y.applyUpdate(this.document, new Uint8Array(row.content as ArrayBuffer));
    }
  }

  async onSave() {
    // Called after each debounced edit window and when the room empties
    this.ctx.storage.sql.exec(
      "INSERT OR REPLACE INTO documents (id, content) VALUES (?, ?)",
      this.name,
      Y.encodeStateAsUpdate(this.document)
    );
  }
}

export default {
  async fetch(request: Request): Promise<Response> {
    return (
      (await routePartykitRequest(request, env)) ||
      new Response("Not Found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;

Client setup

Basic YProvider

// src/client.ts
import YProvider from "y-partyserver/provider";
import * as Y from "yjs";

const yDoc = new Y.Doc();

const provider = new YProvider(
  "localhost:8787",       // host — use window.location.host in production
  "my-document-name",    // document / room name
  yDoc,
  {
    party: "document",   // matches the binding name, kebab-cased
    connect: true,
    // Authenticate if needed
    params: async () => ({
      token: await getAuthToken(),
    }),
  }
);

React integration with useYProvider

// src/App.tsx
import useYProvider from "y-partyserver/react";

function Editor() {
  const provider = useYProvider({
    host: "localhost:8787", // optional, defaults to window.location.host
    room: "my-document-name",
    party: "document",
    // pass an existing Y.Doc if you need to share it with other hooks
    // doc: yDoc,
  });

  // provider.doc is the Yjs document
  // Use it with any editor integration below
  return <MyEditorComponent doc={provider.doc} />;
}

Editor integrations

TipTap

// src/TiptapEditor.tsx
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import useYProvider from "y-partyserver/react";

export function TiptapEditor() {
  const provider = useYProvider({
    room: "my-document",
    party: "document",
  });

  const editor = useEditor({
    extensions: [
      StarterKit,
      Collaboration.configure({
        document: provider.doc,
      }),
    ],
  });

  return <EditorContent editor={editor} />;
}

Monaco

// For Monaco, use the y-monaco binding
// npm install y-monaco
import * as Y from "yjs";
import { MonacoBinding } from "y-monaco";
import YProvider from "y-partyserver/provider";

const yDoc = new Y.Doc();
const yText = yDoc.getText("monaco");
const provider = new YProvider("localhost:8787", "my-doc", yDoc, {
  party: "document",
});

// Pass yText and the Monaco model to the binding
const binding = new MonacoBinding(
  yText,
  editor.getModel()!,
  new Set([editor]),
  provider.awareness
);

Lexical

// For Lexical, use the y-lexical binding
// npm install y-lexical
import * as Y from "yjs";
import { createBinding } from "@lexical/yjs";
import YProvider from "y-partyserver/provider";

const yDoc = new Y.Doc();
const provider = new YProvider("localhost:8787", "my-doc", yDoc, {
  party: "document",
});

// createBinding wires up a Lexical editor to the Yjs document
const binding = createBinding(editor, provider, "root", yDoc, yDoc.getMap("root"));
The Yjs documentation covers all available editor bindings in detail. Examples that use y-websocket as the provider can be replaced with y-partyserver/provider and connected to your DO backend.

Custom messages

You can send arbitrary string messages over the same WebSocket connection alongside Yjs sync data — useful for chat, notifications, or custom function calls.
// src/server.ts
import { YServer } from "y-partyserver";
import type { Connection } from "partyserver";

export class Document extends YServer {
  onCustomMessage(connection: Connection, message: string): void {
    const data = JSON.parse(message);

    if (data.action === "ping") {
      // Reply to the sender only
      this.sendCustomMessage(
        connection,
        JSON.stringify({ action: "pong", timestamp: Date.now() })
      );

      // Broadcast a notification to all other connections
      this.broadcastCustomMessage(
        JSON.stringify({ action: "notification", text: "Someone pinged!" }),
        connection // exclude the sender
      );
    }
  }
}

Custom message API reference

Client (YProvider):
  • provider.sendMessage(message: string) — send a custom string message to the server
  • provider.on("custom-message", handler) — listen for custom messages from the server
Server (YServer):
  • onCustomMessage(connection, message) — override to handle incoming custom messages
  • sendCustomMessage(connection, message) — reply to a specific connection
  • broadcastCustomMessage(message, excludeConnection?) — send to all connections

Real-world fixtures

The PartyKit repository ships three ready-to-run fixtures for the most popular collaborative editors:

TipTap

Rich-text collaborative editing with TipTap’s Collaboration extension and full persistence to Durable Object SQLite storage.

Monaco

VS Code-style code editing powered by Monaco Editor and the y-monaco binding.

Lexical

Facebook’s Lexical editor with @lexical/yjs providing the Yjs integration.
All three fixtures use the same server pattern: extend YServer, implement onLoad / onSave against DO SQLite storage, and re-export the class for the Durable Object binding. Only the client-side editor library changes.

Build docs developers (and LLMs) love