feat(infra): deploy the dashboard to Cloudflare with Alchemy#116

merged4
stack/sveltekit-3
stack/alchemy-cloudflare
opened29d agobyOoliver
View on GitHubTwo-way sync enabledPR last imported 3d ago

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.

  1. Personal stage: bun run deploy:cloudflare --stage <you> (e.g. alice-dev). Expect atlantis-<you> and atlantis-db-<you>, all migrations applied, and a printed worker URL. Seed the D1 database, open the URL, and check a dataset page. Then bun run destroy:cloudflare --stage <you> removes both.
  2. Redeploy: an unchanged redeploy is a no-op; a change under apps/web/src rebuilds and uploads.
  3. Stage names: bun run plan:cloudflare --stage alice_dev fails with Invalid 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.
  4. Schema drift: bun run --cwd apps/web db:generate on an unchanged schema reports No schema changes, nothing to migrate.

Decisions and edge cases

  • Names: prod keeps atlantis / atlantis-db, and destroying prod retains both. Other stages get a -<stage> suffix. Stage names are validated because the old _→- mapping let alice_dev and alice-dev share 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.json layout. 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), so db:generate saw every CHECK as changed and emitted a migration rebuilding all six tables that have CHECKs. The snapshots now store the unqualified form; a test runs drizzle-kit generate against 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, plan included, may offer to create or upgrade that store; plan does not change the stage's worker or database.
  • Removed: wrangler.jsonc, the wrangler deploy and d1:* scripts, and adapter-cloudflare (Alchemy injects its own adapter).
  • Temporary patch: @alchemy.run/frontend-frameworks backports its upstream fix for kit 3.0.0-next.27. Drop it on the next beta.
  • Local D1: alchemy dev can't provide cloudflare:workers under Node, so D1 testing means deploying a personal stage. vite dev fails clearly if asked for D1.
  • Preview: bun run --cwd apps/web preview still rebuilds with SQLite and serves it (from #115). The Playwright suite runs build:web (D1) and then preview, 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:

  1. Record a Time Travel bookmark: bunx wrangler@4.141.0 d1 time-travel info atlantis-db.
  2. Drop every table except _cf_*.
  3. bun run plan:cloudflare --stage prod (database adopted, worker taken over in place, nothing replaced or deleted), then bun run deploy:cloudflare --stage prod. Migrations run on the empty database.
  4. 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 generate on 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:web pass.

Checks

All checks passed

Added 4 commits

  • fix(web): store unqualified CHECK expressions in drizzle snapshots0bf2e89
  • fix(infra): reject noncanonical stage names0e48733
  • docs: tighten the Cloudflare runbook and record the pending cutover75182f5
  • feat(infra): deploy the dashboard to Cloudflare with Alchemycf14c0f