Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.tgz
pnpm add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgz
yarn add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgz
bun add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgz
nub add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgz
aube add https://edytor-docs.beynar.workers.dev/edytor-0.1.0-next.0.tgz

That 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 dev

The 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 deploy

Then 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 authorize returns readOnly: 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.

Was this page helpful?