2.0.0-beta.56 - 200+ Cloudflare Resources
The Cloudflare provider grows from 22 resources to 230 in this release. A single PR (#601) lands +101k lines across 542 files — 189 new live-test suites, every one run against a real Cloudflare account — bringing coverage to over a hundred Cloudflare services: Zero Trust (Access, Tunnels, Devices, Gateway, DLP), Magic Transit and Magic Network Monitoring, Load Balancing, DNS and DNS Firewall, SSL/TLS and certificates, Waiting Rooms, Logpush, Spectrum, Turnstile, API Shield, Bot Management, Page Shield, Workers for Platforms, Snippets, Zaraz, Registrar, Addressing, Alerting, and the rest of the long tail.
That means an entire Zero Trust deployment — tunnel, private network route, identity-gated application, WARP device profile — is now a single Effect (Zero Trust resources contributed by Andy Jefferson in #570):
// a private network reachable only through WARP + Accessconst tunnel = yield* Cloudflare.Tunnel("Corp", { ingress: [ { hostname: "dashboard.example.com", service: "http://localhost:3000" }, { service: "http_status:404" }, ],});
yield* Cloudflare.TunnelRoute("PrivateNet", { tunnelId: tunnel.tunnelId, network: "10.4.0.0/16",});
const allowCorp = yield* Cloudflare.AccessPolicy("AllowCorp", { name: "Allow corp users", decision: "allow", include: [{ emailDomain: { domain: "example.com" } }],});
yield* Cloudflare.AccessApplication("Dashboard", { type: "self_hosted", domain: "dashboard.example.com", sessionDuration: "24h", policies: [allowCorp.policyId],});
yield* Cloudflare.DeviceDefaultProfile("Default", { mode: "exclude", splitTunnelExclude: [{ address: "10.0.0.0/8", description: "RFC1918" }], excludeOfficeIps: true,});How: a test-driven SDK flywheel
Section titled “How: a test-driven SDK flywheel”The resources were implemented by fleets of AI agents working service by service against distilled, our generated Cloudflare SDK. Each agent implements a service’s resources, then live-tests them against the real API — and when a test hits an API behavior the types don’t capture, the fix goes into the SDK as a typed-error patch, not a catch-block in the consumer:
{ "errors": { "WidgetNotFound": [{ "code": 10404 }, { "code": 10407 }], "Forbidden": [{ "status": 403 }] }}Run that loop across 288 live-test suites and the SDK’s patch set grows from 369 to 1,087 operations spanning 94 of 114 Cloudflare services — in one release cycle. Each patch records how the API actually behaves, as a type, for every future consumer of the SDK.
The full story — the loop, the patches, and the factory that ran them — is in Looping the Generation of IaC and SDKs.
Feature flags: Cloudflare.FlagshipApp
Section titled “Feature flags: Cloudflare.FlagshipApp”Cloudflare’s Flagship feature flags land as full resources with an Effect-native Worker binding (#602). Declare the app and its flags:
const app = yield* Cloudflare.FlagshipApp("Flags", {});
yield* Cloudflare.FlagshipFlag("NewCheckout", { appId: app.appId, key: "new-checkout", defaultVariation: "off", variations: { off: false, on: true },});FlagshipApp.bind(app) attaches the binding to the surrounding Worker
and returns a runtime client for evaluating flags:
export const App = Cloudflare.FlagshipApp("Flags", {});
export default Cloudflare.Worker( "FlagsWorker", { main: import.meta.filename }, Effect.gen(function* () { const flags = yield* Cloudflare.FlagshipApp.bind(App); return { fetch: Effect.gen(function* () { const enabled = yield* flags.getBooleanValue("new-checkout", false, { userId: "user-42", }); return HttpServerResponse.text(enabled ? "on" : "off"); }), }; }).pipe(Effect.provide(Cloudflare.FlagshipBindingLive)),);Every method of the runtime Flagship binding is mirrored as an
Effect. Async Workers can bind the app on env instead and use the
raw binding.
Provider.list and alchemy unsafe nuke
Section titled “Provider.list and alchemy unsafe nuke”Providers can now implement a list() lifecycle operation that
enumerates every live resource of their type in the ambient
account/zone
(#620) —
each item returned in the same Attributes shape read produces, so
it’s directly deletable.
Built on top of it: alchemy unsafe nuke
(#624)
enumerates everything a profile’s providers can see and deletes it
all. With 230 resource types now enumerable, a burned test account is
a one-liner to clean:
alchemy unsafe nuke \ --include "Cloudflare.*" \ --filter 'resource.workerName === "alchemy-state-store"' \ --dry-run--include / --exclude take provider-ID globs; --filter takes
JavaScript expressions evaluated with resource in scope — any
resource for which an expression is truthy is spared. --dry-run
previews, --yes skips the confirmation. The command is deliberately
hidden from --help; it deletes real infrastructure.
GitHub repositories and webhook events
Section titled “GitHub repositories and webhook events”Justin Bennett contributed
GitHub.Repository
(#607) —
full repository lifecycle with a converging reconciler that reads by
stable numeric id, so renaming a repo out from under the stack doesn’t
corrupt state:
const repo = yield* GitHub.Repository("internal-tools", { owner: "my-org", name: "internal-tools", visibility: "private", deleteBranchOnMerge: true,});On top of it, GitHub.events turns a repository into an event
source for Cloudflare Workers
(#619).
Subscribing in a Worker’s init provisions a GitHub.Webhook pointed
at the Worker’s URL; at runtime, deliveries are verified with a
constant-time HMAC-SHA256 signature check and handed to your handler
as fully-typed payloads:
const secret = yield* Config.redacted("GITHUB_WEBHOOK_SECRET");
// `event.name` is narrowed to "push" | "pull_request"yield* GitHub.events({ owner: "my-org", repository: "my-repo", events: ["push", "pull_request"], secret,}).subscribe((event) => Effect.log(`received ${event.name} (${event.id})`),);S3-backed state store: AWS.state()
Section titled “S3-backed state store: AWS.state()”AWS stacks get a first-class remote state backend, mirroring
Cloudflare.state()
(#585):
const Stack = Alchemy.Stack( "my-stack", { providers: AWS.providers(), state: AWS.state() }, Effect.gen(function* () { // ... }),);State lives at s3://{bucket}/{prefix}{stack}/{stage}/{fqn}.json. By
default the bucket is an auto-created account-regional bucket
(alchemy-state-{accountId}-{region}-an); pass bucketName /
prefix to use your own. Credential resolution and bucket creation
are deferred to the first state operation, so nothing touches AWS at
layer construction.
Also in this release
Section titled “Also in this release”bundle: falseonCloudflare.Workeruploads a prebuiltmainbyte-for-byte — no rolldown, no minification — with glob-basedrulesselecting additional modules. The working path for OpenNext and other externally-bundled outputs. Thanks Alex (#592).alchemy planstarts near-instant — 8.2s → 1.8s warm, via lazy-loading the TUI and vite plugin and deferring distilled schema construction (#618).worker.urlis stable across deploys when the first domain is unchanged, so downstream resources referencing it (e.g. webhook delivery URLs) stop phantom-updating (#616); same forworker.durableObjectNamespaceswhen the DO class set is unchanged (#617).- Lambda function URLs accept full config —
url: { authType: "AWS_IAM", cors, invokeMode: "RESPONSE_STREAM" }; IAM-auth URLs drop the public permission statement. Thanks José Netto (#614). alchemy devstays alive when an apply fails, logging the full cause instead of silently exiting. Thanks Matthew Aylward (#582).- StaticSite forwards
dev.env(including Redacted secrets) to the external dev-server command. Thanks Alex (#587). - Local assets binding no longer 500s in
alchemy dev(#613). - Drizzle drift detection is cached per plan/apply cycle, so
drizzle-kit’s interactive rename prompt appears once instead of
hanging
alchemy dev(#597).