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.
| Unit | Runs on | Source |
|---|---|---|
| Data & functions | Convex | convex/ at the repo root |
| Workspace relay | Cloudflare Worker + Durable Object | packages/workspace-relay/wrangler.toml |
| Control plane | Cloudflare Worker + Durable Objects + R2 | packages/claxedo-server/wrangler.toml |
| App | Cloudflare Pages | packages/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.
Before you start
Section titled “Before you start”- 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 installrun.
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)Deploy Convex (data & functions)
From the repo root (the Convex functions live in
convex/):Terminal window # Catch config/codegen/type errors before the unrollbackable pushCONVEX_DEPLOY_KEY=... bunx convex deploy --dry-run --typecheck enableCONVEX_DEPLOY_KEY=... bunx convex deploy --typecheck enableDeploy the relay Worker
The relay is a Worker with a
WorkspaceRelayRoomDurable Object holding the (hibernatable) host tunnels. Frompackages/workspace-relay:Terminal window # Verify the bundle compiles — no Cloudflare account needednpx wrangler deploy --dry-run --outdir dist-worker# Secrets, then deploynpx wrangler secret put CLAXEDO_RELAY_RESOLVER_TOKENnpx wrangler secret put CLAXEDO_RELAY_HOST_SIGNING_KEY_PEMnpx wrangler secret put CLAXEDO_CONTROL_PLANE_JWKS_URLnpx wrangler deployTwo
[vars]inwrangler.tomlare deliberate and deployment-critical:CLAXEDO_RELAY_TUNNEL_CHANNEL_CAP— concurrent WebSocket channels per workspace tunnel (production256; 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.
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.pemopenssl pkey -in rat-private.pem -pubout -out rat-public.pem# Preflight: the bundle must compile and be local-execution-freebun test src/worker.import-graph.test.tsnpx wrangler deploy --dry-run --outdir dist-worker# Required secrets — the Worker fails closed (503) at boot without themnpx wrangler secret put CLERK_JWT_ISSUERnpx wrangler secret put CLERK_JWKS_URLnpx wrangler secret put CLAXEDO_WORKSPACE_AUTHORITY_URL # your Convex deployment URLnpx wrangler secret put CLAXEDO_WORKSPACE_RELAY_URL # the relay Worker URL from step 2npx wrangler secret put CLAXEDO_RELAY_RESOLVER_TOKEN # same value installed on the relaynpx wrangler secret put CLAXEDO_RUNTIME_ACCESS_TOKEN_PRIVATE_KEY_PEM # rat-private.pemnpx wrangler secret put CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM # rat-public.pemnpx wrangler secret put CLAXEDO_RUNTIME_ADMIN_TOKEN # random; must differ from the resolver tokennpx wrangler deployCLAXEDO_SIGNED_CLOUD_AUTH = "1"is already set inwrangler.toml[vars]— the hosted shape always runs signed-auth, fail-closed.Shared service token. The same randomly generated
CLAXEDO_CONTROL_PLANE_SERVICE_TOKENmust 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_TOKENDocuments (R2). Hosted Documents needs the
CLAXEDO_DOCUMENTSR2 bucket bound inwrangler.toml:Terminal window npx wrangler r2 bucket create claxedo-documentsnpx wrangler r2 bucket info claxedo-documents --json # verify before deployWithout the binding,
/documents/*returnsdocument_backend_unavailable(503).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 buildcd packages/claxedo-appVITE_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 Pagesfind dist -type f -name '*.map' -deletebunx wrangler pages deploy dist --project-name <your-pages-project> --branch mainVerify 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 planecurl -s "$BASE/api/claxedo/health"# {"ok":true,"mode":"hosted-control-plane","localExecution":false}# 2. The deployment is configured fail-closedcurl -s "$BASE/api/claxedo/mode" # expect signedAuth: true# 3. Signing keys are publishedcurl -s "$BASE/.well-known/jwks.json" # {"keys":[…]}# 4. Auth fails CLOSED: a garbage bearer must yield exactly 401curl -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 upcurl -s https://<your-relay>.workers.dev/healthProbe 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:
npx wrangler secret put CLAXEDO_SANDBOX_DRIVER # e.g. daytonanpx wrangler secret put DAYTONA_API_KEY # driver credentialsnpx wrangler secret put CLAXEDO_DAYTONA_SNAPSHOTOperating it
Section titled “Operating it”Rollback is per-unit, and asymmetric:
| Unit | Rollback | Hard limit |
|---|---|---|
| Control-plane Worker | npx wrangler rollback — seconds | Cannot roll back across a Durable Object migration |
| Relay Worker | npx wrangler rollback — seconds | Same DO limit; a deploy restarts the DO, so host tunnels drop and re-establish |
| Convex | None. Re-deploy the previous green SHA | Only works if every schema change was additive |
| Pages app | Redeploy 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.
How hosted Claxedo automates this
Section titled “How hosted Claxedo automates this”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.