Claude Code for Yjs: Real-Time Collaborative Editing — Claude Skills 360 Blog
Blog / Backend / Claude Code for Yjs: Real-Time Collaborative Editing
Backend

Claude Code for Yjs: Real-Time Collaborative Editing

Published: April 17, 2027
Read time: 7 min read
By: Claude Skills 360

Yjs is a CRDT (conflict-free replicated data type) implementation for building collaborative applications — new Y.Doc() creates a shared document. doc.getMap("state") and doc.getArray("items") create CRDT types that merge conflict-free across peers. new WebsocketProvider(serverUrl, room, doc) syncs via WebSocket. new WebRTCProvider(room, doc) syncs peer-to-peer. provider.awareness manages cursor presence and user metadata. Changes from any peer are automatically merged without conflicts using vector clocks. doc.on("update", (update) => { /* persist or broadcast */ }) listens for changes. Y.applyUpdate(doc, update) applies received updates. new UndoManager(doc.getText("content")) adds undo/redo scoped to a type. Yjs binds to TipTap with @hocuspocus/provider, to Lexical with @lexical/yjs, and to CodeMirror with y-codemirror. y-indexeddb persists documents in the browser. Claude Code generates Yjs doc setup, WebSocket sync servers, awareness cursors, and editor bindings for collaborative applications.

CLAUDE.md for Yjs

## Yjs Stack
- Version: yjs >= 13.6, y-websocket >= 1.5
- Doc: const doc = new Y.Doc(); const map = doc.getMap("data"); const arr = doc.getArray("items")
- Sync: const provider = new WebsocketProvider("ws://localhost:1234", "room", doc)
- Awareness: provider.awareness.setLocalStateField("user", { name, color, cursor })
- Update: doc.on("update", update => broadcast(update)); Y.applyUpdate(doc, remoteUpdate)
- Persist: Y.encodeStateAsUpdate(doc) → store; Y.applyUpdate(doc, storedUpdate) → restore
- TipTap: @hocuspocus/provider with tiptap CollaborationExtension
- UndoMgr: new UndoManager([doc.getText("content")], { trackedOrigins: new Set(["local"]) })

Yjs Document and CRDT Types

// lib/yjs/doc.ts — shared document setup
import * as Y from "yjs"

// Shared document — all collaborative state lives here
export function createBoardDoc() {
  const doc = new Y.Doc()

  // Y.Map — key-value store, conflict-free merge on concurrent updates
  const metadata = doc.getMap<string>("metadata")

  // Nested map via Y.Map of Y.Maps
  const cards = doc.getMap<Y.Map<unknown>>("cards")

  // Y.Array — ordered list with conflict-free insert/delete
  const columns = doc.getArray<string>("columns")

  // Y.Text — conflict-free rich text editing
  const description = doc.getText("description")

  return { doc, metadata, cards, columns, description }
}

// Card operations — all CRDT, conflict-free
export function addCard(
  cards: Y.Map<Y.Map<unknown>>,
  card: { id: string; title: string; columnId: string; order: number },
) {
  const cardMap = new Y.Map<unknown>()
  cardMap.set("id", card.id)
  cardMap.set("title", card.title)
  cardMap.set("columnId", card.columnId)
  cardMap.set("order", card.order)
  cardMap.set("createdAt", Date.now())
  cards.set(card.id, cardMap)
}

export function updateCardTitle(
  cards: Y.Map<Y.Map<unknown>>,
  cardId: string,
  title: string,
) {
  const card = cards.get(cardId)
  if (card) {
    card.set("title", title)
    card.set("updatedAt", Date.now())
  }
}

export function moveCard(
  cards: Y.Map<Y.Map<unknown>>,
  cardId: string,
  newColumnId: string,
  newOrder: number,
) {
  // Transactional — both changes apply atomically
  cards.doc!.transact(() => {
    const card = cards.get(cardId)
    if (card) {
      card.set("columnId", newColumnId)
      card.set("order", newOrder)
    }
  })
}

WebSocket Sync Server

// server/yjs-server.ts — y-websocket sync server (Node.js)
import { WebSocketServer } from "ws"
import { setupWSConnection, docs } from "y-websocket/bin/utils"
import http from "http"
import * as Y from "yjs"
import { db } from "@/lib/db"

const server = http.createServer((req, res) => {
  res.writeHead(200)
  res.end("Yjs WebSocket Server")
})

const wss = new WebSocketServer({ server })

wss.on("connection", (ws, req) => {
  // Extract room from URL: /room-name
  const room = req.url?.slice(1) ?? "default"

  // Load persisted state for this room
  const docName = room
  setupWSConnection(ws, req, {
    docName,
    gc: true,  // Garbage collect deleted items
  })
})

// Persist Yjs updates to database
const persistDoc = async (room: string, doc: Y.Doc) => {
  const update = Y.encodeStateAsUpdate(doc)
  await db.yDoc.upsert({
    where: { room },
    create: { room, data: Buffer.from(update) },
    update: { data: Buffer.from(update), updatedAt: new Date() },
  })
}

// Load persisted docs on connection
const originalGetDoc = (docs as any).get.bind(docs)
;(docs as any).get = async (docName: string) => {
  let doc = originalGetDoc(docName)
  if (!doc._loaded) {
    const saved = await db.yDoc.findUnique({ where: { room: docName } })
    if (saved) {
      Y.applyUpdate(doc, new Uint8Array(saved.data))
    }
    doc._loaded = true
  }
  return doc
}

server.listen(1234)
console.log("Yjs WebSocket server running on :1234")

React Collaboration Hook

// hooks/useCollaborativeDoc.ts — React hook for Yjs
"use client"
import { useEffect, useRef, useState, useCallback } from "react"
import * as Y from "yjs"
import { WebsocketProvider } from "y-websocket"
import type { Awareness } from "y-protocols/awareness"

type User = { name: string; color: string; cursor?: { x: number; y: number } }

export function useCollaborativeBoard(roomId: string, currentUser: User) {
  const docRef = useRef<Y.Doc | null>(null)
  const providerRef = useRef<WebsocketProvider | null>(null)

  const [connected, setConnected] = useState(false)
  const [awarenessUsers, setAwarenessUsers] = useState<Map<number, { user: User }>>(new Map())

  useEffect(() => {
    const doc = new Y.Doc()
    const provider = new WebsocketProvider(
      process.env.NEXT_PUBLIC_YJS_SERVER ?? "ws://localhost:1234",
      `board-${roomId}`,
      doc,
    )

    docRef.current = doc
    providerRef.current = provider

    // Set local awareness state
    provider.awareness.setLocalStateField("user", currentUser)

    provider.on("status", ({ status }: { status: string }) => {
      setConnected(status === "connected")
    })

    // Track connected users via awareness
    const updateAwareness = () => {
      setAwarenessUsers(new Map(provider.awareness.getStates() as Map<number, { user: User }>))
    }

    provider.awareness.on("change", updateAwareness)
    updateAwareness()

    return () => {
      provider.awareness.off("change", updateAwareness)
      provider.destroy()
      doc.destroy()
    }
  }, [roomId, currentUser])

  const updateCursor = useCallback((x: number, y: number) => {
    providerRef.current?.awareness.setLocalStateField("user", {
      ...currentUser,
      cursor: { x, y },
    })
  }, [currentUser])

  return {
    doc: docRef.current,
    cards: docRef.current?.getMap<Y.Map<unknown>>("cards"),
    connected,
    users: awarenessUsers,
    updateCursor,
  }
}

User Cursors Component

// components/collaboration/UserCursors.tsx — show remote cursors
"use client"

type AwarenessUser = { name: string; color: string; cursor?: { x: number; y: number } }

export function UserCursors({
  users,
  localClientId,
}: {
  users: Map<number, { user: AwarenessUser }>
  localClientId: number
}) {
  return (
    <div className="pointer-events-none fixed inset-0 z-50">
      {Array.from(users.entries()).map(([clientId, state]) => {
        if (clientId === localClientId || !state.user?.cursor) return null
        const { cursor, name, color } = state.user

        return (
          <div
            key={clientId}
            className="absolute transition-all duration-100"
            style={{ left: cursor.x, top: cursor.y }}
          >
            {/* Cursor arrow */}
            <svg width="16" height="20" viewBox="0 0 16 20" className="drop-shadow-sm">
              <path d="M0 0 L0 16 L4 12 L8 20 L10 19 L6 11 L12 11 Z" fill={color} />
            </svg>
            {/* Name label */}
            <span
              className="absolute left-4 top-0 text-xs px-1.5 py-0.5 rounded text-white whitespace-nowrap font-medium"
              style={{ backgroundColor: color }}
            >
              {name}
            </span>
          </div>
        )
      })}
    </div>
  )
}

For the Liveblocks alternative when a managed real-time collaboration backend with built-in conflict resolution, REST APIs, webhooks, and a generous free tier are preferred over self-hosting a Yjs WebSocket server — Liveblocks wraps Yjs with a cloud sync layer, see the Liveblocks guide. For the Ably alternative when a general-purpose real-time messaging platform with channels, presence, and history is preferred over CRDT-based collaborative editing — Ably suits real-time chat and notifications better than document collaboration but doesn’t handle merge conflicts automatically, see the Ably guide. The Claude Skills 360 bundle includes Yjs skill sets covering CRDT types, sync providers, and awareness. Start with the free tier to try collaborative editing generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free