Skip to content

Migrate infra to alchemy v2 (Effect-based Stack)#207

Merged
Makisuo merged 16 commits into
mainfrom
alchemy-v2
Jul 14, 2026
Merged

Migrate infra to alchemy v2 (Effect-based Stack)#207
Makisuo merged 16 commits into
mainfrom
alchemy-v2

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What

Migrates the entire deploy layer from alchemy v1 (^0.93.12) to alchemy 2.0.0-beta.61 (the Effect-based rewrite), following https://v2.alchemy.run/migrating-from-v1/ in the full-Effect style:

  • Root alchemy.run.ts is now a single Alchemy.Stack("maple", …) Effect program; the seven app files (api, alerting, chat-flue, electric-sync, web, landing, local-ui) became factories returning Effect.gen programs. v1→v2 mechanics applied throughout: entrypointmain, bindingsenv, alchemy.secretRedacted.make, compatibility: "node"{ date, flags: ["nodejs_compat"] }, per-resource adopt: true→ deploy-wide --adopt.
  • Binding names are preserved verbatim (verified against v2 source) — no worker source changes anywhere.
  • Deploy order change: chat-flue deploys before api, which service-binds it directly (CHAT_FLUE); the v1 WorkerStub cycle-breaker is deleted. Enabled by giving pr stages static custom domains (api-pr-N/chat-pr-N/sync-pr-N.maple.dev in stage.ts) because v2 resource attributes are lazy Outputs that can't be string-interpolated into env.
  • web/landing/local-ui deploy as Command.Build + Worker(assets: {directory, hash}) — all three are static SPA builds with a tiny assets-fallback worker, so v2's Vite pipeline isn't needed. Web's VITE_* env flows through the Build's memo hash, so per-stage URL changes re-run the build. The v1 alchemy/cloudflare/vite plugin (and the now-unused @cloudflare/vite-plugin dep) are removed.
  • api: Hyperdrive.Connection (origin from MAPLE_PG_URL), KV, queue + standalone Queues.Consumer (same batch settings), workflows keyed by physical name; alerting keeps its cross-script workflow ref via scriptName.
  • chat-flue: Command.Build replaces inline execFileSync; bundle: false + module rules for the prebuilt Flue bundle; the AI binding now comes from a Cloudflare.AI.Gateway resource (v2 has no bare Ai() descriptor for async workers).

Why

v2 is the maintained line. v1 state is incompatible with v2 — cutover per stage is: abandon v1 state, redeploy with --adopt (identical physical names re-adopt live resources). Never run a v1 alchemy destroy against a live stage.

Reviewer notes

  • prd is intentionally blocked: v2 has no HyperdriveRef equivalent for the dashboard-managed maple-prd Hyperdrive. The root stack fails fast on --stage prd; prd deploys keep using a v1 checkout until that gap closes (or we adopt maple-prd as a managed Connection with creds in the prd deploy env).
  • effect patch extended (patches/effect@4.0.0-beta.93.patch): bun's isolated linker gives the alchemy CLI and the stack program two path-distinct effect module instances; effect v4's redactedRegistry is a module-local WeakMap, so every Redacted env binding crashed with "Unable to get redacted value". The patch shares the registry via a Symbol.for-keyed global (what effect v3's globalValue did). Candidate for upstreaming to effect-smol.
  • Secrets/state: ALCHEMY_PASSWORD/ALCHEMY_STATE_TOKEN are dead. State is v2's Cloudflare state store (one-time alchemy bootstrap cloudflare per account, or first deploy --yes); ALCHEMY_LOCAL_STATE=1 opts into local file state for dev experiments. CI auth is CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID; the legacy CLOUDFLARE_DEFAULT_ACCOUNT_ID name from Infisical is bridged in the root stack.
  • Deploy scripts gained --yes (v2 prompts otherwise); the pr deploy also passes --adopt. CI workflow YAML needed no changes (env comes from Infisical).
  • New tsconfig.alchemy.json typechecks all eight alchemy.run.ts files, wired into bun run typecheck.

Validation

  • bun run typecheck green (29 turbo tasks + the alchemy tsconfig).
  • CI=true ALCHEMY_LOCAL_STATE=1 bunx alchemy deploy --dry-run --stage dev-claude --env-file .env.local plans all 18 resources to create (5 workers, 2 SPA workers, 3 builds, Hyperdrive, KV, queue + consumer, 2 workflows, AI gateway).
  • Not yet deployed to any cloud stage. First real validation should be a pr-N preview deploy (state-store bootstrap required first), checking: queue consumer settings in the dashboard, both workflows triggerable, chat-flue DO roundtrip + env.AI runtime behavior, api→CHAT_FLUE service call, alerting cross-script workflow start, and PR-close destroy not touching stg/prd zone objects.

🤖 Generated with Claude Code


Open in Devin Review

Rewrites the whole deploy layer from alchemy v1 (^0.93.12) to alchemy
2.0.0-beta.61: the root alchemy.run.ts is now a single
Alchemy.Stack("maple", ...) Effect program and every app factory returns
an Effect.gen program (entrypoint→main, bindings→env, alchemy.secret→
Redacted.make, compat presets→{date, flags}).

Highlights:
- chat-flue deploys before api, which service-binds it directly; the v1
  WorkerStub cycle-breaker is deleted (pr stages now get static custom
  domains api-pr-N/chat-pr-N/sync-pr-N.maple.dev because v2 Outputs are
  lazy and cannot be string-interpolated into env).
- web/landing/local-ui deploy as Command.Build + Worker(assets) — they
  are static SPA builds with a tiny assets-fallback worker, so v2's Vite
  pipeline is not needed; web's VITE_* env flows through the Build memo
  hash so per-stage URL changes re-run the build.
- api: Hyperdrive.Connection, standalone Queues.Consumer, Workflow refs
  keyed by physical name; alerting keeps the cross-script workflow ref.
- chat-flue AI binding now comes from a Cloudflare.AI.Gateway resource
  (v2 has no bare Ai() descriptor for async workers).
- effect patch: share redactedRegistry via a Symbol.for-keyed global —
  bun's isolated linker gives the alchemy CLI and the stack program two
  effect module instances, and the module-local WeakMap made
  Redacted.value throw "Unable to get redacted value".
- ALCHEMY_PASSWORD/ALCHEMY_STATE_TOKEN are gone; state is the v2
  Cloudflare state store (ALCHEMY_LOCAL_STATE=1 opts into local file
  state), CI auth is CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
  (legacy CLOUDFLARE_DEFAULT_ACCOUNT_ID is bridged).
- prd is guarded off: v2 has no HyperdriveRef equivalent for the
  dashboard-managed maple-prd Hyperdrive; deploy prd from a v1 checkout.
- tsconfig.alchemy.json typechecks all alchemy.run.ts files (wired into
  bun run typecheck).

Validated: full typecheck green; dry-run plan on a dev stage creates all
18 resources. No cloud stage has been deployed from this branch yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pullfrog

pullfrog Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

devin-ai-integration[bot]

This comment was marked as resolved.

The dev-stage regex now allows underscores (for alchemy v2's default
dev_$USER stage) but the rejection message still described the old
[a-z0-9-]+ grammar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's fresh isolated install had no root node_modules/effect (locally a
leftover hoist masked it), so the alchemy CLI failed to resolve
'effect/Effect' from the root stack file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Planning the Cloudflare.AI.Gateway resource calls the AI Gateway API,
which CI's scoped deploy token cannot access (Unauthorized). The gateway
was only a vehicle for the { type: "ai" } binding, so attach that
binding directly via worker.bind — exact v1 Ai() parity, zero cloud
calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…ponses; memoize web build on VITE_COMMIT_SHA

Two fixes from the first real pr-preview deploy:

- Cloudflare's put-script response can echo `placement: {"mode":"targeted"}`
  without the region; the @distilled.cloud/cloudflare response union had no
  such variant, so a successful worker upload surfaced as CloudflareHttpError.
  Patch the two response schemas (bun patch) to make `region` optional.
  Candidate for upstreaming.
- Add VITE_COMMIT_SHA to the web Command.Build env so a SHA-only change
  busts the build memo instead of serving a cached bundle with a stale
  `deployment.commit_sha` (Devin review finding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… binding

The raw worker.bind escape hatch was a workaround for the CI token
missing the AI Gateway permission; the token is being extended with
account-level 'AI Gateway: Edit' instead, so use the v2-native resource.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Underscored dev stages (alchemy's default dev_${USER}) flowed raw into
Cloudflare worker/Hyperdrive names, which only allow [a-z0-9-].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utes

Zone routes don't create DNS records — api-pr-N/sync-pr-N.maple.dev were
authoritative NXDOMAIN, so the deployed preview web app couldn't reach
either worker (ERR_NAME_NOT_RESOLVED). Custom domains provision DNS and
edge certs automatically, matching how chat/web/landing already deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generic "Cannot reach Maple API" block repeated identically in every
widget during an outage: chart cards rendered the left-aligned inline
variant floating in a 280px card, the glyph was three near-invisible
bars, and the stat cards used a bespoke "Unable to load" string.

- ErrorState: new DroppedSignalGlyph (signal line that cuts out, pulsing
  destructive break point, dashed expected trail) in the QuietMonitor
  mini-readout idiom; inline variant gets a tinted chip + ghost button
  instead of a red dot + underlined link; panel fills its container.
- metrics-grid: chart-card errors use the centered panel variant
  (border-0) instead of inline floating in the card.
- service-usage-cards: error branch keeps the card rhythm with an
  em-dash value slot + "Couldn't load" caption, matching the loaded
  state's header typography.
- error-messages: transport-error copy no longer says "try again"
  directly above the Try again button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…hemy v2

deploy-prd.yml triggered on every push to main, but the v2 stack program
fails fast on --stage prd (HyperdriveRef gap), so every merge would have
produced a red prd deploy. Restore the push trigger when prd cuts over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Every other worker sets one; landing only set the nodejs_compat flag and
would have deployed on the platform default date.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…veRef parity)

v2 has no HyperdriveRef, so stg/prd attach MAPLE_DB as raw
{ type: "hyperdrive", id } binding metadata via worker.bind — the
dashboard owns the origin credentials, deploys never see them, and
MAPLE_PG_URL is only required for pr/dev stages (alchemy-managed
per-branch Hyperdrive). Removes the prd fail-fast guard: prd can now
plan/deploy on v2.

TEMPORARY: staging shares prod's `maple-prd` config (owner decision) —
stg workers hit the production database until a dedicated `maple-stg`
config exists; see resolveHyperdriveRefId.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…over era

The first v2 deploy against a stage with live v1-created resources needs
adoption-on-conflict; the pr script already passed it. Remove once both
stages are owned by v2 state (adoption restores fail-loud semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Makisuo
Makisuo merged commit d052f84 into main Jul 14, 2026
8 checks passed
@Makisuo
Makisuo deleted the alchemy-v2 branch July 14, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant