Migrate infra to alchemy v2 (Effect-based Stack)#207
Merged
Conversation
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>
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 →
|
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>
…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>
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>
…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>
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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:alchemy.run.tsis now a singleAlchemy.Stack("maple", …)Effect program; the seven app files (api,alerting,chat-flue,electric-sync,web,landing,local-ui) became factories returningEffect.genprograms. v1→v2 mechanics applied throughout:entrypoint→main,bindings→env,alchemy.secret→Redacted.make,compatibility: "node"→{ date, flags: ["nodejs_compat"] }, per-resourceadopt: true→ deploy-wide--adopt.CHAT_FLUE); the v1WorkerStubcycle-breaker is deleted. Enabled by giving pr stages static custom domains (api-pr-N/chat-pr-N/sync-pr-N.maple.devinstage.ts) because v2 resource attributes are lazy Outputs that can't be string-interpolated into env.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'sVITE_*env flows through the Build's memo hash, so per-stage URL changes re-run the build. The v1alchemy/cloudflare/viteplugin (and the now-unused@cloudflare/vite-plugindep) are removed.Hyperdrive.Connection(origin fromMAPLE_PG_URL), KV, queue + standaloneQueues.Consumer(same batch settings), workflows keyed by physical name; alerting keeps its cross-script workflow ref viascriptName.Command.Buildreplaces inlineexecFileSync;bundle: false+ module rules for the prebuilt Flue bundle; theAIbinding now comes from aCloudflare.AI.Gatewayresource (v2 has no bareAi()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 v1alchemy destroyagainst a live stage.Reviewer notes
HyperdriveRefequivalent for the dashboard-managedmaple-prdHyperdrive. The root stack fails fast on--stage prd; prd deploys keep using a v1 checkout until that gap closes (or we adoptmaple-prdas a managedConnectionwith creds in the prd deploy env).patches/effect@4.0.0-beta.93.patch): bun's isolated linker gives the alchemy CLI and the stack program two path-distincteffectmodule instances; effect v4'sredactedRegistryis a module-local WeakMap, so everyRedactedenv binding crashed with "Unable to get redacted value". The patch shares the registry via aSymbol.for-keyed global (what effect v3'sglobalValuedid). Candidate for upstreaming to effect-smol.ALCHEMY_PASSWORD/ALCHEMY_STATE_TOKENare dead. State is v2's Cloudflare state store (one-timealchemy bootstrap cloudflareper account, or firstdeploy --yes);ALCHEMY_LOCAL_STATE=1opts into local file state for dev experiments. CI auth isCLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID; the legacyCLOUDFLARE_DEFAULT_ACCOUNT_IDname from Infisical is bridged in the root stack.--yes(v2 prompts otherwise); the pr deploy also passes--adopt. CI workflow YAML needed no changes (env comes from Infisical).tsconfig.alchemy.jsontypechecks all eightalchemy.run.tsfiles, wired intobun run typecheck.Validation
bun run typecheckgreen (29 turbo tasks + the alchemy tsconfig).CI=true ALCHEMY_LOCAL_STATE=1 bunx alchemy deploy --dry-run --stage dev-claude --env-file .env.localplans all 18 resources to create (5 workers, 2 SPA workers, 3 builds, Hyperdrive, KV, queue + consumer, 2 workflows, AI gateway).env.AIruntime 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