Deployment
Production ships from main:
push main├─ Cloudflare Git integration → www├─ Cloudflare Git integration → docs└─ GitHub Actions → backstage on FlyAll three deploy from the pushed commit. Backstage and www deploy only once bun run test passes, manual deploys included: the backstage deploy script refuses a commit without a green Test run on GitHub, and the www deploy script runs the suite before building, in Workers Builds or locally (docs is not gated). Database migrations remain manual. To stand up another instance, start at self-hosting.
Manual deploys are for targeted releases. Backstage requires a clean checkout matching origin/main (or the matching main commit in GitHub Actions), and uploads an archive of that commit so concurrent checkout edits cannot enter its build:
bunx varlock load # names every missing secret; fill them in the root .env.localbun install # also needs `fly auth login`bun run deploy # backstage → docs → www, verified after eachbun run deploy www docs # or a slice; --list shows targetsbun run push-migrations # schema — separate and manualscripts/deploy.ts owns target order, preflight, deploy, and verification. bun run deploy with no targets ships backstage + www + docs only. Before any selected service deploys, each selected Cloudflare Worker must pass a non-interactive wrangler deployments list --json access check using its own config. wrangler whoami is not sufficient: it can exit successfully while logged out. A failed access check blocks the whole run; sign in with bunx wrangler login or supply a token with access to the selected Workers.
Cloudflare Workers Builds (CI)
@arbe/www → worker arbe, @arbe/www-docs → arbe-docs. Push to main deploys these two through Cloudflare’s Git integrations; the GitHub build workflow deploys the Fly backstage from the same commit, in parallel with its bundle checks. Manual bun run deploy is reserved for hotfixes and targeted deploys.
Builds has a production deploy command and a separate non-production one (PR previews / version uploads). Config lives under apps/* while the root directory stays / for workspace deps — the dashboard default npx wrangler versions upload fails here (no root wrangler.jsonc).
| Worker | Production deploy | Non-production / PR preview |
|---|---|---|
arbe | bun run --filter '@arbe/www' deploy | bun run --filter '@arbe/www' deploy:preview |
arbe-docs | bun run --filter '@arbe/www-docs' deploy | npx wrangler versions upload --config apps/docs/wrangler.jsonc (build command bun run --filter '@arbe/www-docs' build) |
The account is on the Workers Free plan, so Builds runs one build at a time on 2 vCPUs. The docs build usually starts first and www waits for it: of www’s 2–3 minutes after a push, 35–130s is that queue, ~20–30s clone and install, ~35s the full test suite, ~16–25s vite build and ~13–16s the upload. The suite is CPU-bound there, which is why core and www run Vitest in the vmThreads pool (it halved the suite on 2 cores locally); taskset -c 0,1 bun run test reproduces the build box locally. Read a build’s stages from cf builds list (queue = running_on − created_on) and its log timestamps (below).
Two Builds connections on the same repo / main, one per worker — Builds injects WRANGLER_CI_OVERRIDE_NAME with the connected worker’s name, so a single connection ships docs onto arbe. Both use root directory /, an empty build command except the docs preview above (the root build script builds the CLI binary; the other deploy / preview commands run their own build), and no watch paths — a wrong one ships a stale worker silently, and arbe-docs renders repo-root docs/, not just apps/docs. The dashboard shows only the production build command; the non-production one lives on its own trigger, so read and change it with cf builds triggers list --external-script-id <tag> and cf builds triggers update <trigger-uuid> --build-command ... (Worker tags are under the post-push check below).
build.yml builds both production bundles on every push to main and every PR, independent of Workers Builds. On pushes to main, one job builds and pushes the backstage image while the tests run (deploy:build), and another releases it to Fly once the Test job passes (deploy:release, which rechecks the gate); neither waits for the bundle builds, which never reach the image. An idle backstage exits as soon as SIGINT arrives; only in-flight turns hold the swap, for up to the 20s drain grace. gh workflow run build.yml --ref <branch> runs the checks and tests on any branch without deploying. The repository needs a FLY_API_TOKEN Actions secret created with fly tokens create deploy -a arbe-backstage. bun run check does not bundle (svelte-check type-checks only) and the Vite dev server resolves differently, so a broken rollup build — a dependency override the app has outgrown, a missing export — is otherwise invisible until someone deploys. The workflow supplies placeholder values for the six worker build-time secrets; the bundles only need them to link. The one generated runtime module, packages/sandbox/src/daytona/generated/bundles.ts, is gitignored: the root postinstall writes it on every bun install (so CI, Workers Builds and fresh clones have it) and the backstage image builds its own. The build image pins bun 1.2.15 unless a connection sets BUN_VERSION. Both connections must set it to the bun that wrote bun.lock (1.4.2 as of 2026-09-13, lockfile version 2); an older image fails within seconds with UnknownLockfileVersion and lockfile had changes, but lockfile is frozen, while the GitHub build (bun-version: latest) passes. Bumping bun locally means bumping BUN_VERSION on both connections in the same change.
Only arbe has secrets. They all live in the Builds connection now: varlock-wrangler resolves the schema from them at deploy time and uploads the result as worker vars/secrets — environment-variables has the details.
Both deploy scripts stamp the commit in --message; arbe deploys through varlock-wrangler and puts RELEASE_SHA in the process env so varlock uploads it for analytics stamping (track.ts). Both deploy scripts prefer release identity sources in this order: ARBE_RELEASE_SHA, then Builds’ WORKERS_CI_COMMIT_SHA, then Git. Source archives must set ARBE_RELEASE_SHA; the manual deploy refuses an unknown identity. The manual docs deploy captures that identity before building, passes it as ARBE_RELEASE_SHA, and verifies the exact same deployment message, including when WORKERS_CI_COMMIT_SHA differs from the checkout.
Post-push check: did a build actually run?
The GitHub Action and two Workers Builds run independently. Confirm each deploy before assuming all of main is live.
-
Dashboard: Cloudflare → worker
arbe→ Build history. The Deployment history page cannot answer this — every row there says “Wrangler” because CI runs our ownbun run deploy. -
CLI:
gh api repos/oskarrough/arbe/commits/<sha>/check-runsshows aWorkers Builds: arbecheck. It lags thearbe-docscheck by a couple of minutes, so do not call it missing too early.cf builds list(below) shows the same builds with status and commit. -
Deployment message: both CI and manual deploys stamp the resolved commit SHA; its length does not tell you which path deployed it.
-
Build log: read it with Cloudflare’s
cfCLI;wranglerhas no builds command.cfasks which account when the login spans several, andbuilds listtakes the Worker’s tag, not its name (arbeise80c088a4d9f4ad6b3548faed140180f,arbe-docsisec779e40659c453f9a2ee21f617b310b;cf workers scripts search --name arbelists them):Terminal window export CLOUDFLARE_ACCOUNT_ID=b29de5cf62555d67d57242aeddc91344cf builds list --external-script-id e80c088a4d9f4ad6b3548faed140180f --per-page 3 # newest first: build_uuid, status, build_outcome, commit_hashcf builds logs get <build_uuid> # one page of lines; pass the returned --cursor for the next -
Note that a green
build.ymlrun is not this check: it proves the rollups link and deploys backstage, but it does not schedule either Workers Build.
On 10–11 Sep both builds failed for three pushes in a row: the root postinstall ran the sandbox bundling step, which exited 1 with no output inside the build image (bun 1.2.15, the same binary that passed locally). The hook is back now that the connections pin a newer bun; if a Workers Build fails at @arbe/sandbox build again, read the FAIL: bundling … line (it prints the bun version and what Bun.build returned) and the dashboard log, then raise it with Cloudflare.
CLI binaries ship to Cloudflare R2 at https://downloads.arbe.0sk.ar/ via the cli-release GitHub Action — see releases for the build/upload/version contract. The arbe-downloads bucket is on the same account with a custom domain attached via R2 → Settings → Custom Domains (no worker fronts it). Repo secrets needed: CLOUDFLARE_ACCOUNT_ID (the account id above — not sensitive) and CLOUDFLARE_API_TOKEN (Object Read & Write scoped to arbe-downloads only — rotate if leaked). Set both at GitHub → Settings → Secrets and variables → Actions.
R2 bucket
| Bucket | Holds | Read by |
|---|---|---|
arbe-downloads | published CLI tarballs + manifests | cli-release Action (write), https://downloads.arbe.0sk.ar/ (public read) |
No application worker has an R2 binding.
Supabase auth is configured in the dashboard, not in code:
- Auth → URL Configuration: add
http://localhost:5173/auth/callback(local) andhttps://arbe.0sk.ar/auth/callback(prod). - Auth → Providers → GitHub: enable, paste Client ID + Secret from the GitHub OAuth App.
- GitHub OAuth App callback URL:
https://<project-ref>.supabase.co/auth/v1/callback.
Code: scripts/deploy.ts, .github/workflows/build.yml, apps/*/wrangler.jsonc, apps/backstage/fly.toml, .github/workflows/cli-release.yml.
See system/environment-variables, system/development, releases.