Server quick start
Deploy the edytor Durable Object room on Cloudflare and connect your editor to it in a few steps.
Edytor ships its sync server as a Cloudflare Durable Object: DocumentRoom coordinates one document, and routeDocumentSocket is the Worker-side door that authorizes each connection before it reaches the room. You deploy both on your own Cloudflare account. This page takes you from an empty folder to a running room.
Create a Worker project
Create a TypeScript Worker (for example with pnpm create cloudflare@latest edytor-room, choosing the “Hello World” Worker template), then add edytor:
npm install https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgzpnpm add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgzyarn add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgzbun add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgznub add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgzaube add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgzThat is the pre-release tarball this site hosts; once it is on npm, install edytor@next instead (see Installation). The room only imports edytor/cloudflare, which runs in Workers: no Svelte, no DOM.
Declare the Durable Object
The room stores the document in SQLite, so declare it as a SQLite class in wrangler.jsonc:
{
"name": "edytor-room",
"main": "src/worker.ts",
"compatibility_date": "2026-09-26",
"durable_objects": {
"bindings": [{ "name": "ROOMS", "class_name": "DocumentRoom" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["DocumentRoom"] }]
}Write the Worker
Export the room class and route WebSocket upgrades through routeDocumentSocket. Each document id gets its own room.
import { DocumentRoom, requestedReplica, routeDocumentSocket } from 'edytor/cloudflare';
export { DocumentRoom };
type Env = { ROOMS: DurableObjectNamespace<DocumentRoom> };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const match = /^\/rooms\/([^/]+)$/.exec(new URL(request.url).pathname);
if (!match) return new Response('not found', { status: 404 });
return routeDocumentSocket(request, env.ROOMS, decodeURIComponent(match[1]), (request) => {
// Development only: trusts whoever says they are `?user=…`.
const userId = new URL(request.url).searchParams.get('user');
if (!userId) return null; // → closed 4403, the room is never reached
return { userId, replica: requestedReplica(request) };
});
}
} satisfies ExportedHandler<Env>;Run it locally
npx wrangler devThe room is now at ws://localhost:8787/rooms/<documentId>. Wrangler runs the Durable Object and its SQLite storage locally.
Connect the editor
Point the view at /rooms, use the document id as the room, and send the user as a parameter.
<script lang="ts">
import { Edytor } from 'edytor';
let { userId, documentId }: { userId: string; documentId: string } = $props();
</script>
<Edytor
server="ws://localhost:8787/rooms"
room={documentId}
params={{ user: userId }}
actor={{ id: userId, name: userId }}
/>Open the page in two browsers with different users: edits, carets and selections appear in both. In production, send a token instead of the user id (params: { token }) and derive the user from it in authorize.
Deploy
npx wrangler deployThen switch server to wss://<your-worker-host>/rooms.
What you get
- Real-time sync between every client of a document, with offline edits merged on reconnect.
- Durable storage: every update is written to the room’s SQLite storage before it is acknowledged, so the provider’s saved state means “stored on the server”.
- Identity: each client id is bound to the user who first wrote under it; nobody can write in someone else’s name.
- Read-only access when
authorizereturnsreadOnly: true. - Hibernation: the room holds no timers, so idle rooms hibernate with their sockets still connected.
The room does not decide who may open a document; your authorize does. See Authorization and the room.
See it live
The live editor on the edytor home page runs on exactly this setup: a room Worker deployed at edytor-room.beynar.workers.dev with DocumentRoom and routeDocumentSocket. Its authorize admits guests by an id their browser keeps, it opens one room per UTC day so the shared document resets daily, and it only accepts sockets from the docs’ origins. It turns the other dials away with closedSocket (4404 for a closed room, 4403 for another origin), so a page left open past the reset stops dialing instead of retrying forever. Open the home page in two windows to watch them sync, and read its source for a complete, deployed example.