Skills yjs
Expert guidance for Yjs, the high-performance CRDT (Conflict-free Replicated Data Type) framework for building collaborative applications. Helps developers implement real-time document editing, offline-first sync, and peer-to-peer collaboration with automatic conflict resolution.
git clone https://github.com/TerminalSkills/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/yjs" ~/.claude/skills/terminalskills-skills-yjs && rm -rf "$T"
skills/yjs/SKILL.md- references .env files
Yjs — CRDT Framework for Collaborative Editing
Overview
Yjs, the high-performance CRDT (Conflict-free Replicated Data Type) framework for building collaborative applications. Helps developers implement real-time document editing, offline-first sync, and peer-to-peer collaboration with automatic conflict resolution.
Instructions
Document and Shared Types
Create collaborative data structures that merge automatically:
// src/collaboration/document.ts — Set up a collaborative document with shared types import * as Y from "yjs"; // A Y.Doc is the top-level container for all shared data // Every connected client gets a copy that stays in sync const doc = new Y.Doc(); // Y.Text — collaborative rich text (used with editors like Tiptap, ProseMirror) const yText = doc.getText("document-content"); yText.insert(0, "Hello, "); yText.insert(7, "world!"); // Result: "Hello, world!" — inserts merge correctly even if concurrent // Y.Map — collaborative key-value store (like a shared object) const yMap = doc.getMap("settings"); yMap.set("theme", "dark"); yMap.set("fontSize", 14); // Two users setting different keys: both applied // Two users setting the same key: last-write-wins (deterministic) // Y.Array — collaborative ordered list const yArray = doc.getArray("tasks"); yArray.push([{ id: "1", title: "Design mockup", done: false }]); yArray.push([{ id: "2", title: "Implement API", done: false }]); // Concurrent inserts at different positions: both preserved in correct order // Y.XmlFragment — collaborative XML tree (for rich text editors) const yXml = doc.getXmlFragment("rich-content"); // Used internally by editor bindings (Tiptap, ProseMirror, Slate) // Nested structures — Y types can be nested arbitrarily const yNestedMap = new Y.Map(); yNestedMap.set("status", "active"); yMap.set("project", yNestedMap); // Map inside a map
WebSocket Provider
Connect clients through a WebSocket server for real-time sync:
// src/collaboration/provider.ts — WebSocket-based real-time sync import * as Y from "yjs"; import { WebsocketProvider } from "y-websocket"; const doc = new Y.Doc(); // Connect to a y-websocket server // All clients in the same room sync automatically const provider = new WebsocketProvider( "wss://your-yjs-server.example.com", // WebSocket server URL "document-room-123", // Room name — clients in same room sync doc, { connect: true, // Auto-connect on creation params: { token: "auth-token-here" }, // Auth params sent on connect } ); // Awareness — lightweight presence data (cursors, selections, user info) // Unlike document state, awareness is ephemeral (not persisted) const awareness = provider.awareness; awareness.setLocalStateField("user", { name: "Alice", color: "#ff5733", cursor: null, }); // Listen to other users' awareness changes awareness.on("change", () => { const states = awareness.getStates(); // Map<clientId, state> states.forEach((state, clientId) => { if (clientId !== doc.clientID) { console.log(`User ${state.user?.name} is connected`); } }); }); // Connection status provider.on("status", ({ status }: { status: string }) => { console.log(`Connection: ${status}`); // "connecting" | "connected" | "disconnected" }); // Sync status — fires when initial sync with server is complete provider.on("sync", (isSynced: boolean) => { if (isSynced) console.log("Document fully synced with server"); });
Server-Side Setup
Run a y-websocket server for document persistence:
// server/yjs-server.ts — WebSocket server with persistence import { WebSocketServer } from "ws"; import { setupWSConnection, setPersistence } from "y-websocket/bin/utils"; import * as Y from "yjs"; import { MongodbPersistence } from "y-mongodb-provider"; const wss = new WebSocketServer({ port: 1234 }); // Persist documents to MongoDB (survives server restarts) const mdb = new MongodbPersistence(process.env.MONGODB_URL!, { collectionName: "yjs-documents", flushSize: 100, // Batch 100 updates before flushing to DB multipleCollections: true, // Separate collection per document for performance }); setPersistence({ bindState: async (docName: string, ydoc: Y.Doc) => { // Load existing document state from MongoDB const persistedDoc = await mdb.getYDoc(docName); const persistedState = Y.encodeStateAsUpdate(persistedDoc); Y.applyUpdate(ydoc, persistedState); // Save updates as they happen ydoc.on("update", (update: Uint8Array) => { mdb.storeUpdate(docName, update); }); }, writeState: async (docName: string, ydoc: Y.Doc) => { // Called when all clients disconnect — final persistence await mdb.flushDocument(docName); }, }); wss.on("connection", (ws, req) => { // Authenticate the connection const token = new URL(req.url!, "http://localhost").searchParams.get("token"); if (!verifyToken(token)) { ws.close(4001, "Unauthorized"); return; } setupWSConnection(ws, req); }); console.log("Yjs WebSocket server running on port 1234");
Editor Integration (Tiptap)
Add collaborative editing to a Tiptap rich text editor:
// src/components/CollaborativeEditor.tsx — Tiptap with Yjs collaboration 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 * as Y from "yjs"; import { WebsocketProvider } from "y-websocket"; interface Props { documentId: string; userName: string; userColor: string; } export function CollaborativeEditor({ documentId, userName, userColor }: Props) { const doc = useMemo(() => new Y.Doc(), []); const provider = useMemo( () => new WebsocketProvider("wss://yjs.example.com", documentId, doc), [doc, documentId] ); const editor = useEditor({ extensions: [ StarterKit.configure({ history: false, // Disable default history — Yjs handles undo/redo }), Collaboration.configure({ document: doc, // Bind editor content to Yjs document }), CollaborationCursor.configure({ provider, // Share cursor positions via awareness user: { name: userName, color: userColor }, }), ], }); // Clean up on unmount useEffect(() => { return () => { provider.destroy(); doc.destroy(); }; }, [doc, provider]); return ( <div className="editor-container"> <EditorContent editor={editor} /> <ConnectionStatus provider={provider} /> </div> ); } function ConnectionStatus({ provider }: { provider: WebsocketProvider }) { const [status, setStatus] = useState("connecting"); useEffect(() => { const handler = ({ status }: { status: string }) => setStatus(status); provider.on("status", handler); return () => provider.off("status", handler); }, [provider]); return ( <div className={`status-badge ${status}`}> {status === "connected" ? "🟢 Connected" : "🔴 Reconnecting..."} </div> ); }
Offline Support and Sync
Handle offline editing with automatic merge on reconnect:
// src/collaboration/offline.ts — IndexedDB persistence for offline support import * as Y from "yjs"; import { IndexeddbPersistence } from "y-indexeddb"; import { WebsocketProvider } from "y-websocket"; const doc = new Y.Doc(); // IndexedDB provider — saves document locally in the browser // Changes made offline are preserved and synced when reconnected const indexedDb = new IndexeddbPersistence("my-app-docs", doc); indexedDb.on("synced", () => { console.log("Local data loaded from IndexedDB"); }); // WebSocket provider — syncs with other clients when online const wsProvider = new WebsocketProvider("wss://yjs.example.com", "doc-123", doc); // The two providers work together: // 1. Online: changes sync via WebSocket AND save to IndexedDB // 2. Offline: changes save to IndexedDB only // 3. Reconnect: IndexedDB state syncs with server, merging all changes // Observe document changes (from any source: local, remote, or loaded from DB) doc.on("update", (update: Uint8Array, origin: any) => { if (origin === "local") { console.log("Local change"); } else { console.log("Remote change received"); } });
Installation
# Core library npm install yjs # Providers (pick what you need) npm install y-websocket # WebSocket sync npm install y-indexeddb # Browser offline persistence npm install y-webrtc # Peer-to-peer sync (no server) # Editor bindings npm install @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor # Server persistence npm install y-mongodb-provider # MongoDB npm install y-leveldb # LevelDB (lightweight)
Examples
Example 1: Setting up Yjs with a custom configuration
User request:
I just installed Yjs. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.
Example 2: Extending Yjs with custom functionality
User request:
I want to add a custom websocket provider to Yjs. How do I build one?
The agent scaffolds the extension/plugin project, implements the core functionality following Yjs's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.
Guidelines
- Choose the right shared type — Y.Text for documents, Y.Map for settings/state, Y.Array for lists; don't force everything into one type
- Keep documents small — Large Y.Docs (>10MB) impact performance; split content into multiple documents
- Use awareness for ephemeral data — Cursors, selections, and typing indicators belong in awareness, not document state
- Always add offline persistence — y-indexeddb prevents data loss on disconnect; it's one line of code
- Authenticate at the provider level — Validate tokens in the WebSocket server before allowing sync
- Batch observations — Use
to group multiple changes into one update eventdoc.transact() - Garbage collect — Call
to enable garbage collection of deleted contentdoc.gc = true - Test concurrent edits — Open multiple browser tabs and edit simultaneously; verify merge behavior