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.
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.tsimport { 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>;
// src/client.tsimport 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(), }), });
// src/App.tsximport 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} />;}
// For Monaco, use the y-monaco binding// npm install y-monacoimport * 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 bindingconst binding = new MonacoBinding( yText, editor.getModel()!, new Set([editor]), provider.awareness);
// For Lexical, use the y-lexical binding// npm install y-lexicalimport * 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 documentconst 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.
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.tsimport { 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 ); } }}
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.