Skip to content

Yjs

Use crdtRoom() with Yjs when your client already produces Yjs updates. Send binary updates as base64 strings so the backend can store them safely and keep the hot room state optimized inside Durable Objects.

Terminal window
app.post("/documents/:documentId/yjs", async (request) => {
const pathSegments = new URL(request.url).pathname.split("/")
const documentId = pathSegments[2]
const body = await request.json()
return await live.crdtRoom(documentId).applyYjsUpdate({
updateBase64: body.updateBase64,
clientId: body.clientId,
clock: body.clock,
})
})

Create a short-lived room grant on the backend and return it to the browser:

Terminal window
app.post("/documents/:documentId/socket", async (request) => {
const pathSegments = new URL(request.url).pathname.split("/")
const documentId = pathSegments[2]
const body = await request.json()
return await live.crdtRoom(documentId).authorize({
clientId: body.clientId,
presence: body.presence,
})
})

Use the returned URL and protocols in the browser WebSocket constructor:

Terminal window
const grant = await fetch(`/documents/${documentId}/socket`, {
method: "POST",
body: JSON.stringify({
clientId,
presence: { cursor: 12 },
}),
}).then((response) => response.json())
const socket = new WebSocket(grant.url, grant.protocols)
socket.send(JSON.stringify({
type: "yjs.update",
updateBase64,
}))
socket.send(JSON.stringify({
type: "presence",
state: { cursor: 18 },
}))

The grant is bound to the room, selected actor, client id, initial presence, and expiration time.

Clients can send their current Yjs state vector and receive the missing update:

Terminal window
app.post("/documents/:documentId/sync", async (request) => {
const pathSegments = new URL(request.url).pathname.split("/")
const documentId = pathSegments[2]
const body = await request.json()
return await live.crdtRoom(documentId).syncYjs({
stateVectorBase64: body.stateVectorBase64,
limit: 200,
})
})

The result includes a merged updateBase64, the current stateVectorBase64, recent update records, and awareness records.

Yjs document updates and awareness are separate. Store cursors, selections, and online editor state with awareness():

Terminal window
await live.crdtRoom("document_123").awareness({
clientId: "client_1",
state: {
cursor: 12,
selection: { from: 4, to: 12 },
},
})

Use compact() to ask the room to summarize accumulated Yjs updates into a checkpoint:

Terminal window
const checkpoint = await live.crdtRoom("document_123").compact()

Realtime keeps recent update history in the Database product and keeps the hot merged update and state vector in the room Durable Object. This gives active documents fast sync while preserving product-level history.

Yjs room WebSockets use Durable Object hibernation. When a document has no active traffic, Cloudflare can hibernate the Durable Object while keeping clients connected. On wakeup, Realtime restores the socket attachments and the room can continue broadcasting Yjs updates without requiring every client to rejoin first.