feat(infra): deploy the dashboard to Cloudflare with Alchemy#116
Warning
🤖 Claude Opus 5.5 on behalf of Oliver. This changes how production deploys. Production cutover is pending. Do not deploy the prod stage until the cutover below is done.
ELI5
The Cloudflare setup is now a TypeScript program (Alchemy) instead of a wrangler config file. Any copy of the site, such as a throwaway test copy, can be created and deleted with one command.
Flows to exercise
Setup: a Cloudflare API token and account ID exported as CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID, and a small verified SQLite database to seed from.
- Personal stage:
bun run deploy:cloudflare --stage <you>(e.g.alice-dev). Expectatlantis-<you>andatlantis-db-<you>, all migrations applied, and a printed worker URL. Seed the D1 database, open the URL, and check a dataset page. Thenbun run destroy:cloudflare --stage <you>removes both. - Redeploy: an unchanged redeploy is a no-op; a change under
apps/web/srcrebuilds and uploads. - Stage names:
bun run plan:cloudflare --stage alice_devfails withInvalid stage 'alice_dev'. … for example 'alice-dev'.Stage names are lowercase letters, digits, and single hyphens. Note that Alchemy's default stage (live_$USER) is rejected, so always pass--stage. - Schema drift:
bun run --cwd apps/web db:generateon an unchanged schema reportsNo schema changes, nothing to migrate.
Decisions and edge cases
- Names:
prodkeepsatlantis/atlantis-db, and destroyingprodretains both. Other stages get a-<stage>suffix. Stage names are validated because the old_→-mapping letalice_devandalice-devshare resources; with D1 adopt-by-name, destroying one could delete the other's database. - Drizzle moves to
1.0.0-rc.5-ab785fc, the build Alchemy pins, because Alchemy rejects the old_journal.jsonlayout. Migration SQL is byte-identical. The converted snapshots stored table-qualified CHECK expressions ("t"."col" > 0) while the new serializer emits unqualified ones ("col" > 0), sodb:generatesaw every CHECK as changed and emitted a migration rebuilding all six tables that have CHECKs. The snapshots now store the unqualified form; a test runsdrizzle-kit generateagainst a copy of the migrations and fails if it emits anything. - State lives in Alchemy's Cloudflare state store so collaborators share it. Any command,
planincluded, may offer to create or upgrade that store;plandoes not change the stage's worker or database. - Removed:
wrangler.jsonc, the wrangler deploy andd1:*scripts, and adapter-cloudflare (Alchemy injects its own adapter). - Temporary patch:
@alchemy.run/frontend-frameworksbackports its upstream fix for kit3.0.0-next.27. Drop it on the next beta. - Local D1:
alchemy devcan't providecloudflare:workersunder Node, so D1 testing means deploying a personal stage.vite devfails clearly if asked for D1. - Preview:
bun run --cwd apps/web previewstill rebuilds with SQLite and serves it (from #115). The Playwright suite runsbuild:web(D1) and thenpreview, so every e2e run covers that path on this layer too. - Docs pin Wrangler 4.141.0 for the D1 and Time Travel commands.
Production cutover (pending)
Not done in this PR. atlantis-db holds an older schema with an empty d1_migrations table, so a plain deploy would fail with table datasets already exists. Plan:
- Record a Time Travel bookmark:
bunx wrangler@4.141.0 d1 time-travel info atlantis-db. - Drop every table except
_cf_*. bun run plan:cloudflare --stage prod(database adopted, worker taken over in place, nothing replaced or deleted), thenbun run deploy:cloudflare --stage prod. Migrations run on the empty database.- Reload the pipeline data. The reload tooling is still to be decided.
Rollback: bunx wrangler@4.141.0 d1 time-travel restore atlantis-db --bookmark=<bookmark>. The same status and steps are in docs/user/operations.md.
Follow-ups
- Narrow the Cloudflare API token to the permissions the stack needs after the cutover.
- Decide the SQLite→D1 reload tooling.
Verification
Automated: bun run format, lint, typecheck, test:web, test:infra (stage validation), test:e2e, landing lint, and build:landing pass; db:generate produces no migration.
Earlier manual checks (before the review fixes): a throwaway stage applied migrations, was seeded from a public sample dataset, and served /, a dataset page, and six API routes byte-identical to local SQLite. An unchanged redeploy was a no-op, destroy removed the stage, and a read-only prod plan showed adopt/in-place takeover with nothing replaced or deleted.
Remaining manual: re-run flow 1 with a hyphenated stage name, and the production cutover itself.
Claude Opus 5.5 · Claude Code (T3 Code)
Rebased onto the merged MAAD stack
- Migrations 0003–0006 from the MAAD stack are converted to the drizzle v1 folder layout with
drizzle-kit up. Their SQL is byte-identical, and their CHECK expressions are unqualified. drizzle-kit generateon the final schema reports no changes.- The cutover runbook now says all seven migrations run on an empty D1, followed by a data reload built with the current schema.
- Local checks: 198 web tests, 16 infra tests, landing lint/build and
build:webpass.
Checks
- SuccessfulPlaywright E2EGitHub
- SuccessfulWeb ChecksGitHub
- SuccessfulRust Pipeline ChecksGitHub