> ## Documentation Index
> Fetch the complete documentation index at: https://help.plannotator.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy Workspaces on Your Cloudflare Account

> Create the Cloudflare resources, then deploy the required Workspaces app Worker and usercontent Worker.

The account owner must complete a successful Cloudflare Access sign-in on the
custom app domain before the deployment is ready. Back up D1, Git history, and
R2 before serving customer data.

The Cloudflare deployment uses two Workers and three origins. The app Worker
serves the Access-protected browser origin and a public API origin for API-key
requests. The public, cookieless usercontent Worker serves raw file bytes. Both
Workers use the same D1, KV, R2, and Artifacts resources.

## Prerequisites

You need:

* a Cloudflare account with Workers, D1, KV, R2, Artifacts, Durable Objects,
  and Zero Trust;
* a Cloudflare zone for the app, API, and usercontent custom domains;
* Node, `pnpm`, and authenticated `wrangler` access to your Cloudflare account;
  and
* a clean checkout of the `container-v0.2.0` source tag.

Enable R2 and Artifacts in the Cloudflare dashboard before you start. Without
Artifacts, document storage and history operations fail even if `/healthz`
returns `ok: true`.

Install the locked dependencies from the repository root:

```bash theme={null}
pnpm install --frozen-lockfile
```

## Understand the three origins

| Origin             | Access                                           | Purpose                                             |
| ------------------ | ------------------------------------------------ | --------------------------------------------------- |
| App origin         | Protected by Cloudflare Access                   | Browser app, browser API requests, and live editing |
| API origin         | Public; Workspaces API keys protect private data | API, MCP, and read-only Git for tools and agents    |
| Usercontent origin | Public; do not put it behind Access              | Living and exact-version raw file bytes             |

The app and API origins route to the same app Worker. Protect only the browser
app origin with Access. You must also deploy the usercontent Worker. If a raw
URL reaches the app Worker, it returns the browser app HTML instead of the
requested file.

The self-host templates do not include the rate limits used by hosted
Workspaces. If the public API origin allows anonymous creation, add a Cloudflare
WAF rule to its custom domain to limit abuse.

## Create the shared resources

Run every command block on this page from the repository root. The examples use
the resource names shown below. If you change a name, update every later command
and both Worker configs. Record resource IDs outside the repository.

```bash theme={null}
W=apps/rooms/node_modules/.bin/wrangler

$W d1 create workspaces-db
$W kv namespace create workspaces-cache
$W r2 bucket create workspaces-assets
$W r2 bucket create workspaces-text-mirror

$W artifacts repos create bootstrap --namespace workspaces-artifacts
$W artifacts repos delete bootstrap --namespace workspaces-artifacts --force
$W artifacts namespaces get workspaces-artifacts
```

The Artifacts create command can print a bearer token. Treat all command output
from that step as secret. Do not paste it into a log, ticket, or screenshot.

## Configure and deploy the app Worker

<Steps>
  <Step title="Copy the app template">
    ```bash theme={null}
    cp apps/rooms/wrangler.cloudflare-self-host.example.jsonc \
       apps/rooms/wrangler.self-host.jsonc
    ```

    Git ignores the copied file. Keep only placeholders in the tracked
    template.
  </Step>

  <Step title="Set the app bindings">
    Fill in the app Worker name, D1 ID, KV ID, both R2 bucket names, Artifacts
    namespace, source commit, app origin, API origin, usercontent origin,
    instance organization ID, and organization name. Set `database_name` to the
    exact D1 name you created, such as `workspaces-db`, in addition to setting
    `database_id`. Keep separate custom-domain routes for the app and API
    origins.

    Set `AUTH_MODE` to `cloudflare_access`. Add the Access team domain and
    application audience from [Configure Cloudflare
    Access](/workspaces/self-hosting/cloudflare/configure-access).
  </Step>

  <Step title="Apply D1 migrations">
    ```bash theme={null}
    (
      cd apps/rooms
      ./node_modules/.bin/wrangler d1 migrations apply workspaces-db \
        --remote -c wrangler.self-host.jsonc
    )
    ```

    Re-running this command applies only pending migrations.
  </Step>

  <Step title="Set the app secrets">
    ```bash theme={null}
    (
      cd apps/rooms
      CFG=wrangler.self-host.jsonc
      openssl rand -hex 32 | ./node_modules/.bin/wrangler secret put SHARE_LINK_SECRET -c $CFG
      openssl rand -hex 32 | ./node_modules/.bin/wrangler secret put API_KEY_PEPPER -c $CFG
      ./node_modules/.bin/wrangler secret list -c $CFG
    )
    ```

    Store the values only as Worker Secrets. Do not place them in the config.
  </Step>

  <Step title="Build the browser app">
    From the repository root, run:

    ```bash theme={null}
    pnpm build:web:rooms
    ```
  </Step>

  <Step title="Deploy the app Worker">
    ```bash theme={null}
    (
      cd apps/rooms
      ./node_modules/.bin/wrangler deploy -c wrangler.self-host.jsonc
    )
    ```
  </Step>
</Steps>

## Configure and deploy the usercontent Worker

<Steps>
  <Step title="Copy the usercontent template">
    ```bash theme={null}
    cp apps/usercontent/wrangler.cloudflare-self-host.example.jsonc \
       apps/usercontent/wrangler.self-host.jsonc
    ```
  </Step>

  <Step title="Use the same data resources">
    Set a different Worker name and a different custom domain. Copy the exact
    D1 name, D1 ID, KV ID, both R2 bucket names, Artifacts namespace, and source
    commit used by the app Worker. The `database_name` and `database_id` values
    must match the app Worker config.

    Do not add app secrets, Durable Objects, sessions, or D1 migrations to this
    Worker.
  </Step>

  <Step title="Deploy the usercontent Worker">
    ```bash theme={null}
    (
      cd apps/usercontent
      ./node_modules/.bin/wrangler deploy -c wrangler.self-host.jsonc
    )
    ```
  </Step>

  <Step title="Set the usercontent origin">
    Set `USERCONTENT_ORIGIN` in the app Worker config to the usercontent origin.
    Redeploy the app Worker so new raw links use that host.
  </Step>
</Steps>

## Verify the deployment

1. Request `/healthz` on the app, API, and usercontent origins. Confirm all three
   report the same source commit.
2. Complete [Cloudflare Access
   setup](/workspaces/self-hosting/cloudflare/configure-access) and sign in on
   the app origin.
3. Create a Markdown document and copy its raw link.
4. Request the living and exact-version raw URLs from the usercontent origin.
   Both should return `200` with `text/markdown` and the document bytes.
5. Confirm the same raw path on the app origin does not serve the file.
6. Create a test API key after sign-in. Use it for a request to the public API
   origin, revoke it, and confirm the next request returns `401 unauthorized`.
7. Open the document in two browser windows and confirm live editing connects.

A new Worker origin can return a short-lived error immediately after its first
deploy. Retry for up to one minute before changing the configuration.

<Warning>
  Cloudflare Access and custom domains require dashboard setup. The account
  owner must complete a successful human sign-in before the deployment is
  ready.
</Warning>

## Optional product telemetry

The self-host template has no `TELEMETRY` binding, so Workspaces records no
product telemetry and reports nothing to Plannotator. To opt in, add an
Analytics Engine dataset binding named `TELEMETRY` to the app Worker. The data
then stays in your Cloudflare account. It contains event names and broad
categories, not account IDs, workspace IDs, document IDs, paths, or URLs.

*Reviewed July 22, 2026. Maintained by the Plannotator documentation team.*
