Skip to content
View as .md

Deployment

Production ships from main:

push main
├─ Cloudflare Git integration → www
├─ Cloudflare Git integration → docs
└─ GitHub Actions → backstage on Fly

All three deploy from the pushed commit. 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:

Terminal window
bunx varlock load # names every missing secret; fill them in the root .env.local
bun install # also needs `fly auth login`
bun run deploy # backstage → docs → www, verified after each
bun run deploy www docs # or a slice; --list shows targets
bun run push-migrations # schema — separate and manual

scripts/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-docsarbe-docs. Push to main deploys these two through Cloudflare’s Git integrations; the GitHub build workflow deploys the Fly backstage from the same commit after both bundle checks pass. 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).

WorkerProduction deployNon-production / PR preview
arbebun run --filter '@arbe/www' deploybun run --filter '@arbe/www' deploy:preview
arbe-docsbun run --filter '@arbe/www-docs' deploybun run --filter '@arbe/www-docs' build && bunx wrangler versions upload --config apps/docs/wrangler.jsonc

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 (the root build script builds the CLI binary; the deploy / preview commands run their own), and no watch paths — a wrong one ships a stale worker silently, and arbe-docs renders repo-root docs/, not just apps/docs.

build.yml builds both production bundles on every push to main and every PR, independent of Workers Builds. On pushes to main, it then deploys @arbe/backstage to Fly. 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. It also runs bun run --filter '@arbe/sandbox' check:drift: the commit carries one generated runtime module, packages/sandbox/src/daytona/generated/bundles.ts, and nothing rebuilds it on install (the backstage image regenerates it inside its own build), so a stale copy fails CI rather than shipping. Rebuild it with that package’s build script. 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 arbeBuild history. The Deployment history page cannot answer this — every row there says “Wrangler” because CI runs our own bun run deploy.
  • CLI: gh api repos/oskarrough/arbe/commits/<sha>/check-runs shows a Workers Builds: arbe check. It lags the arbe-docs check by a couple of minutes, so do not call it missing too early. wrangler has no builds command at all.
  • Deployment message: both CI and manual deploys stamp the resolved commit SHA; its length does not tell you which path deployed it.
  • Build log: the dashboard is the only place it exists — wrangler has no builds command at all, and the OAuth token wrangler login writes carries no Workers Builds scope, so GET /accounts/<id>/builds/builds/<uuid>/logs is a 403 and gh api .../check-runs reports pass or fail with nothing else. Export the log from Build history when you need to read it.
  • Note that a green build.yml run 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). That hook is gone and the bundle is committed, so the same symptom now points at the image itself — capture the push time and the dashboard log, then raise it with Cloudflare. One failed build on its own is not worth chasing.

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

BucketHoldsRead by
arbe-downloadspublished CLI tarballs + manifestscli-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:

  1. Auth → URL Configuration: add http://localhost:5173/auth/callback (local) and https://arbe.0sk.ar/auth/callback (prod).
  2. Auth → Providers → GitHub: enable, paste Client ID + Secret from the GitHub OAuth App.
  3. 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.