# feat\(infra\): deploy the dashboard to Cloudflare with Alchemy · PR \#116 · flambo/atlantis

[View on GitCafe](https://git.cafe/flambo/atlantis/pull/116)

Repository: [flambo/atlantis](https://git.cafe/flambo/atlantis)

Visibility: public

State: merged

Source repository: flambo/atlantis

Source branch: stack/alchemy-cloudflare

Target branch: stack/sveltekit-3

Head commit: 75182f5692cbb4979c7e3b03bc8a681a6e740ad5

[Discussion and reviews](https://git.cafe/flambo/atlantis/pull/116)

[Commits](https://git.cafe/flambo/atlantis/pull/116/commits?format=markdown)

[Files changed](https://git.cafe/flambo/atlantis/pull/116/diffs?format=markdown)

## Description

---

> [!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.

