---
title: Server quick start
description: Deploy the edytor Durable Object room on Cloudflare and connect your editor to it in a few steps.
icon: rocket
---

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.

1. **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:

    ```package-install
    pnpm 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](/docs/getting-started#install)). The room only imports `edytor/cloudflare`, which runs in Workers: no Svelte, no DOM.

2. **Declare the Durable Object**

    The room stores the document in SQLite, so declare it as a SQLite class in `wrangler.jsonc`:

    ```jsonc 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"] }]
    }
    ```

3. **Write the Worker**

    Export the room class and route WebSocket upgrades through `routeDocumentSocket`. Each document id gets its own room.

    ```ts src/worker.ts check
    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>;
    ```

    :::warning
    This `authorize` accepts any user id from the URL. Replace it with a real session or token check before you deploy anything people use; see [Authorization](/docs/server/authorization).
    :::

4. **Run it locally**

    ```bash
    npx wrangler dev
    ```

    The room is now at `ws://localhost:8787/rooms/<documentId>`. Wrangler runs the Durable Object and its SQLite storage locally.

5. **Connect the editor**

    Point the view at `/rooms`, use the document id as the room, and send the user as a parameter.

    ```svelte src/lib/Editor.svelte check
    <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`.

6. **Deploy**

    ```bash
    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](/docs/collaboration/websocket#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](/docs/server/authorization) and [the room](/docs/server/room).

:::tip
Already have a Durable Object, or want the document in R2, KV or D1 too? Call `attachDocument(this, { onLoad, onSave })` in your own class instead of deploying `DocumentRoom`. See [Your own Durable Object](/docs/server/extending).
:::

## See it live

The live editor on the [edytor home page](/docs) 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`](/docs/server/authorization#authorize) (`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](https://github.com/beynar/edytor/tree/master/site/room) for a complete, deployed example.
