Skip to content
Guide Reference Download app

Full stack on Cloudflare

This guide deploys the same shape hosted Claxedo runs in production: four units on managed infrastructure, released in a fixed order. Nothing here is a simplified demo topology — it mirrors the hosted release pipeline (deploy-control-plane.yml) unit for unit.

UnitRuns onSource
Data & functionsConvexconvex/ at the repo root
Workspace relayCloudflare Worker + Durable Objectpackages/workspace-relay/wrangler.toml
Control planeCloudflare Worker + Durable Objects + R2packages/claxedo-server/wrangler.toml
AppCloudflare Pagespackages/claxedo-app

Workspace execution is not in this table on purpose: agent work runs on workspace hosts (a user’s machine, or cloud sandboxes via a driver) that connect to this stack through the relay. The Worker is a pure control plane — the local-execution modules are never bundled into it, and an import-graph test enforces that.

  • A Cloudflare account (Workers paid plan — the control plane uses Durable Objects and R2) and an API token with Workers + Pages edit rights.
  • A Convex project (one deployment for this environment) and its deploy key.
  • A Clerk application — the hosted stack’s signed-auth identity provider. You need its JWT issuer URL, JWKS URL, and (for the app) the publishable key.
  • An EdDSA keypair for runtime-token signing (generate below).
  • The repo checked out, bun install run.

The deploy order is a contract, not a preference — most-backward-compatible first, so each unit can serve the one deployed before it:

1. Convex → 2. relay Worker → 3. control-plane Worker → 4. app (Pages)
  1. Deploy Convex (data & functions)

    From the repo root (the Convex functions live in convex/):

    Terminal window
    # Catch config/codegen/type errors before the unrollbackable push
    CONVEX_DEPLOY_KEY=... bunx convex deploy --dry-run --typecheck enable
    CONVEX_DEPLOY_KEY=... bunx convex deploy --typecheck enable
  2. Deploy the relay Worker

    The relay is a Worker with a WorkspaceRelayRoom Durable Object holding the (hibernatable) host tunnels. From packages/workspace-relay:

    Terminal window
    # Verify the bundle compiles — no Cloudflare account needed
    npx wrangler deploy --dry-run --outdir dist-worker
    # Secrets, then deploy
    npx wrangler secret put CLAXEDO_RELAY_RESOLVER_TOKEN
    npx wrangler secret put CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM
    npx wrangler secret put CLAXEDO_CONTROL_PLANE_JWKS_URL
    npx wrangler deploy

    Two [vars] in wrangler.toml are deliberate and deployment-critical:

    • CLAXEDO_RELAY_TUNNEL_CHANNEL_CAP — concurrent WebSocket channels per workspace tunnel (production 256; the code default of 16 is below what one real workspace opens).
    • CLAXEDO_RELAY_LOCATION_HINT — where new relay Durable Objects are placed. A DO’s location is fixed at first creation, permanently. Set this to your user base’s region before anyone connects.
  3. Deploy the control-plane Worker

    The control plane owns auth, workspace ownership, relay target resolution, token minting, WorkGraph, Documents (R2), and Connections. From packages/claxedo-server:

    Terminal window
    # Generate the runtime-token signing keypair (EdDSA)
    openssl genpkey -algorithm ed25519 -out rat-private.pem
    openssl pkey -in rat-private.pem -pubout -out rat-public.pem
    # Preflight: the bundle must compile and be local-execution-free
    bun test src/worker.import-graph.test.ts
    npx wrangler deploy --dry-run --outdir dist-worker
    # Required secrets — the Worker fails closed (503) at boot without them
    npx wrangler secret put CLERK_JWT_ISSUER
    npx wrangler secret put CLERK_JWKS_URL
    npx wrangler secret put CLAXEDO_WORKSPACE_AUTHORITY_URL # your Convex deployment URL
    npx wrangler secret put CLAXEDO_WORKSPACE_RELAY_URL # the relay Worker URL from step 2
    npx wrangler secret put CLAXEDO_RELAY_RESOLVER_TOKEN # same value installed on the relay
    npx wrangler secret put CLAXEDO_RUNTIME_ACCESS_TOKEN_PRIVATE_KEY_PEM # rat-private.pem
    npx wrangler secret put CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM # rat-public.pem
    npx wrangler secret put CLAXEDO_RUNTIME_ADMIN_TOKEN # random; must differ from the resolver token
    npx wrangler deploy

    CLAXEDO_SIGNED_CLOUD_AUTH = "1" is already set in wrangler.toml [vars] — the hosted shape always runs signed-auth, fail-closed.

    Shared service token. The same randomly generated CLAXEDO_CONTROL_PLANE_SERVICE_TOKEN must exist on both runtimes:

    Terminal window
    CONVEX_DEPLOY_KEY=... bunx convex env set CLAXEDO_CONTROL_PLANE_SERVICE_TOKEN "$TOKEN"
    printf '%s' "$TOKEN" | npx wrangler secret put CLAXEDO_CONTROL_PLANE_SERVICE_TOKEN

    Documents (R2). Hosted Documents needs the CLAXEDO_DOCUMENTS R2 bucket bound in wrangler.toml:

    Terminal window
    npx wrangler r2 bucket create claxedo-documents
    npx wrangler r2 bucket info claxedo-documents --json # verify before deploy

    Without the binding, /documents/* returns document_backend_unavailable (503).

  4. Deploy the app to Pages

    The app build is pointed at the Worker and Clerk instance at build time:

    Terminal window
    bun run --cwd packages/workgraph build
    cd packages/claxedo-app
    VITE_CLAXEDO_SERVER_URL=https://<your-control-plane>.workers.dev \
    VITE_AUTH_ENABLED=true \
    VITE_CLERK_PUBLISHABLE_KEY=pk_... \
    VITE_CONVEX_URL=https://<your-deployment>.convex.cloud \
    bun run build
    # Never serve raw source maps from Pages
    find dist -type f -name '*.map' -delete
    bunx wrangler pages deploy dist --project-name <your-pages-project> --branch main
  5. Verify the deployment

    These are the same probes the hosted release pipeline gates on — each asserts a behavior, not just that a URL responds:

    Terminal window
    BASE=https://<your-control-plane>.workers.dev
    # 1. Health, and proof this Worker is a pure control plane
    curl -s "$BASE/api/claxedo/health"
    # {"ok":true,"mode":"hosted-control-plane","localExecution":false}
    # 2. The deployment is configured fail-closed
    curl -s "$BASE/api/claxedo/mode" # expect signedAuth: true
    # 3. Signing keys are published
    curl -s "$BASE/.well-known/jwks.json" # {"keys":[…]}
    # 4. Auth fails CLOSED: a garbage bearer must yield exactly 401
    curl -s -o /dev/null -w "%{http_code}\n" \
    -H "authorization: Bearer garbage" \
    "$BASE/api/workgraph/snapshot?limit=1" # 401 — anything else is a failure
    # 5. Relay is up
    curl -s https://<your-relay>.workers.dev/health

    Probe 4 matters most: a 200 from a garbage token means the auth path is not failing closed — the hosted pipeline fails the release on any status other than 401.

Optional: cloud sandboxes for hosted execution

Section titled “Optional: cloud sandboxes for hosted execution”

The stack above serves user-hosted workspaces out of the box (hosts register through the CLI and tunnel into the relay). To also provision cloud workspaces, configure a sandbox driver on the control-plane Worker — the Worker accepts cloudflare, daytona, or fetch:

Terminal window
npx wrangler secret put CLAXEDO_SANDBOX_DRIVER # e.g. daytona
npx wrangler secret put DAYTONA_API_KEY # driver credentials
npx wrangler secret put CLAXEDO_DAYTONA_SNAPSHOT

Rollback is per-unit, and asymmetric:

UnitRollbackHard limit
Control-plane Workernpx wrangler rollback — secondsCannot roll back across a Durable Object migration
Relay Workernpx wrangler rollback — secondsSame DO limit; a deploy restarts the DO, so host tunnels drop and re-establish
ConvexNone. Re-deploy the previous green SHAOnly works if every schema change was additive
Pages appRedeploy a previous build

Durable Object migrations ship solo. A Worker deploy carrying a [[migrations]] entry must contain nothing else — Workers cannot roll back across a DO migration, so bundling one with feature code poisons the rollback well for the whole deploy.

Relay deploys drop tunnels. A relay deploy (or crash) drops active host tunnels and long-lived sessions until hosts reconnect — a bounded seconds-to-a-minute window. Batch relay changes and deploy them at a published time rather than continuously.

Keep secrets stable across code deploys. Wrangler deploys code; secrets and Convex env vars are provisioned separately and persist. Changing them is a deliberate operational act, not part of a release.

The hosted deployment runs exactly this sequence from GitHub Actions (.github/workflows/deploy-control-plane.yml): push to dev → Convex → relay Worker → control-plane Worker → authenticated smoke → Pages app, with staging automatic and production human-gated behind a required-reviewer environment. All deploy workflows share one concurrency group so releases never interleave. If you’re running this stack for a team, replicating that pipeline (deploy from CI, never from a laptop; smoke gates between units) is the recommended end state — the workflow files in the repo are the reference implementation.