Yjs
Sync Yjs CRDT documents over Durable Streams using plain HTTP — no WebSocket infrastructure needed.
y-durable-streams provides a Yjs provider and server that handle snapshot discovery, live updates via long-polling or SSE, automatic server-side compaction, and optional awareness (presence) for cursors and user status.
Installation
npm install @durable-streams/y-durable-streams yjs y-protocols lib0yjs, y-protocols, and lib0 are peer dependencies.
Quick start
import { YjsProvider } from "@durable-streams/y-durable-streams"
import * as Y from "yjs"
import { Awareness } from "y-protocols/awareness"
const doc = new Y.Doc()
const awareness = new Awareness(doc)
const provider = new YjsProvider({
doc,
baseUrl: "http://localhost:4438/v1/yjs/my-service",
docId: "my-document",
awareness,
})
provider.on("synced", (synced) => {
console.log("Synced:", synced)
})The provider connects automatically, discovers the latest snapshot, loads it, then streams live updates.
Provider options
interface YjsProviderOptions {
doc: Y.Doc // Yjs document to sync
baseUrl: string // Server URL, e.g. "http://localhost:4438/v1/yjs/my-service"
docId: string // Document ID (can include slashes, e.g. "project/chapter-1")
awareness?: Awareness // Optional awareness for presence
headers?: HeadersRecord // Optional auth headers
liveMode?: "sse" | "long-poll" // Live update transport (default: "sse")
connect?: boolean // Auto-connect on construction (default: true)
}Events
// Sync state changes
provider.on("synced", (synced: boolean) => {
if (synced) {
console.log("Document is synced with server")
}
})
// Connection status changes
provider.on("status", (status: "disconnected" | "connecting" | "connected") => {
console.log("Status:", status)
})
// Error handling
provider.on("error", (error: Error) => {
console.error("Provider error:", error)
})Lifecycle
// Manual connection
const provider = new YjsProvider({
doc,
baseUrl,
docId,
connect: false, // Don't connect automatically
})
provider.on("synced", handleSync)
provider.on("error", handleError)
await provider.connect()
// Disconnect temporarily
provider.disconnect()
// Reconnect
await provider.connect()
// Destroy permanently
provider.destroy()Always call destroy() when done to clean up event listeners and close connections.
Authentication
const provider = new YjsProvider({
doc,
baseUrl: "http://localhost:4438/v1/yjs/my-service",
docId: "my-document",
awareness,
headers: {
Authorization: "Bearer your-token",
},
})Awareness (presence)
Pass an Awareness instance to enable presence support. y-durable-streams broadcasts local awareness state (cursors, selections, user info) and subscribes to remote awareness updates via a separate SSE stream.
import * as Y from "yjs"
import { Awareness } from "y-protocols/awareness"
import { YjsProvider } from "@durable-streams/y-durable-streams"
const doc = new Y.Doc()
const awareness = new Awareness(doc)
const provider = new YjsProvider({
doc,
baseUrl: "http://localhost:4438/v1/yjs/my-service",
docId: "my-document",
awareness,
})
// Set local user presence
awareness.setLocalStateField("user", {
name: "Alice",
color: "#ff0000",
})
// Listen for remote awareness changes
awareness.on("change", () => {
const states = awareness.getStates()
console.log("Online users:", states.size)
})Awareness heartbeats are sent every 15 seconds. When the provider disconnects, it broadcasts a removal so other clients see the user go offline immediately.
How it works
y-durable-streams uses a four-step sync protocol over HTTP. For the full wire format and details, see the Yjs Durable Streams Protocol specification.
- Snapshot discovery — Requests
?offset=snapshot. The server responds with a 307 redirect to the latest snapshot offset, or to-1if no snapshot exists. - Snapshot loading — Fetches the binary Yjs snapshot and applies it to the local document. The response includes a
stream-next-offsetheader indicating where to continue. - Live updates — Streams incremental updates from the offset via long-polling or SSE. Local edits are sent through an idempotent producer for exactly-once delivery.
- Awareness — An optional separate SSE stream carries presence data (cursors, selections, user info) using named awareness channels.
Compaction
y-durable-streams automatically compacts documents when accumulated updates exceed a size threshold. Compaction creates a new snapshot at the current offset, keeping initial sync fast for new clients. This is transparent to connected clients — existing connections continue uninterrupted.
URL structure
Each document is accessed via a single URL with query parameters:
{baseUrl}/docs/{docPath}?{queryParams}Where docPath can include forward slashes (e.g., project/chapter-1).
Best practices
Always call destroy(). Clean up providers when unmounting components or leaving documents.
useEffect(() => {
const provider = new YjsProvider({ doc, baseUrl, docId, awareness })
return () => provider.destroy()
}, [])Use hierarchical document paths. Organize documents with forward slashes for logical grouping.
// Good
docId: "org/project/chapter-1"
// Also works
docId: "simple-doc"Handle errors gracefully. y-durable-streams automatically reconnects on transient failures, but listen for errors to update the UI.
provider.on("error", (error) => {
showToast("Connection issue — retrying...")
})Learn more
- Yjs protocol specification — full protocol spec
- Package README — complete API reference
- Yjs demo — collaborative text editor example
- Yjs docs — Yjs documentation