| 1 | --- |
| 2 | title: 2.0.0-beta.56 - 200+ Cloudflare Resources |
| 3 | date: 2026-06-17T20:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.56 grows the Cloudflare provider from 22 resources to 230, each implemented and live-tested against the real Cloudflare API — plus `alchemy unsafe nuke`, GitHub webhook event sources, and an S3-backed state store. |
| 6 | --- |
| 7 | |
| 8 | The Cloudflare provider grows from **22 resources to 230** in this |
| 9 | release. A single PR ([#601](https://github.com/alchemy-run/alchemy/pull/601)) |
| 10 | lands +101k lines across 542 files — 189 new live-test suites, every |
| 11 | one run against a real Cloudflare account — bringing coverage to over |
| 12 | a hundred Cloudflare services: Zero Trust (Access, Tunnels, Devices, |
| 13 | Gateway, DLP), Magic Transit and Magic Network Monitoring, Load |
| 14 | Balancing, DNS and DNS Firewall, SSL/TLS and certificates, Waiting |
| 15 | Rooms, Logpush, Spectrum, Turnstile, API Shield, Bot Management, Page |
| 16 | Shield, Workers for Platforms, Snippets, Zaraz, Registrar, Addressing, |
| 17 | Alerting, and the rest of the long tail. |
| 18 | |
| 19 | That means an entire Zero Trust deployment — tunnel, private network |
| 20 | route, identity-gated application, WARP device profile — is now a |
| 21 | single Effect (Zero Trust resources contributed by |
| 22 | **[Andy Jefferson](https://github.com/microagi-andy)** in |
| 23 | [#570](https://github.com/alchemy-run/alchemy/pull/570)): |
| 24 | |
| 25 | ```typescript |
| 26 | // a private network reachable only through WARP + Access |
| 27 | const tunnel = yield* Cloudflare.Tunnel("Corp", { |
| 28 | ingress: [ |
| 29 | { hostname: "dashboard.example.com", service: "http://localhost:3000" }, |
| 30 | { service: "http_status:404" }, |
| 31 | ], |
| 32 | }); |
| 33 | |
| 34 | yield* Cloudflare.TunnelRoute("PrivateNet", { |
| 35 | tunnelId: tunnel.tunnelId, |
| 36 | network: "10.4.0.0/16", |
| 37 | }); |
| 38 | |
| 39 | const allowCorp = yield* Cloudflare.AccessPolicy("AllowCorp", { |
| 40 | name: "Allow corp users", |
| 41 | decision: "allow", |
| 42 | include: [{ emailDomain: { domain: "example.com" } }], |
| 43 | }); |
| 44 | |
| 45 | yield* Cloudflare.AccessApplication("Dashboard", { |
| 46 | type: "self_hosted", |
| 47 | domain: "dashboard.example.com", |
| 48 | sessionDuration: "24h", |
| 49 | policies: [allowCorp.policyId], |
| 50 | }); |
| 51 | |
| 52 | yield* Cloudflare.DeviceDefaultProfile("Default", { |
| 53 | mode: "exclude", |
| 54 | splitTunnelExclude: [{ address: "10.0.0.0/8", description: "RFC1918" }], |
| 55 | excludeOfficeIps: true, |
| 56 | }); |
| 57 | ``` |
| 58 | |
| 59 | ## How: a test-driven SDK flywheel |
| 60 | |
| 61 | The resources were implemented by fleets of AI agents working |
| 62 | service by service against |
| 63 | [distilled](https://github.com/alchemy-run/distilled), our generated |
| 64 | Cloudflare SDK. Each agent implements a service's resources, then |
| 65 | live-tests them against the real API — and when a test hits an API |
| 66 | behavior the types don't capture, the fix goes into the SDK as a |
| 67 | typed-error patch, not a catch-block in the consumer: |
| 68 | |
| 69 | ```json |
| 70 | // submodules/distilled/packages/cloudflare/patches/turnstile/getWidget.json |
| 71 | { |
| 72 | "errors": { |
| 73 | "WidgetNotFound": [{ "code": 10404 }, { "code": 10407 }], |
| 74 | "Forbidden": [{ "status": 403 }] |
| 75 | } |
| 76 | } |
| 77 | ``` |
| 78 | |
| 79 | Run that loop across 288 live-test suites and the SDK's patch set |
| 80 | grows from 369 to **1,087 operations spanning 94 of 114 Cloudflare |
| 81 | services** — in one release cycle. Each patch records how the API |
| 82 | actually behaves, as a type, for every future consumer of the SDK. |
| 83 | |
| 84 | The full story — the loop, the patches, and the factory that ran |
| 85 | them — is in |
| 86 | [Looping the Generation of IaC and SDKs](/blog/2026-07-02-cloudflare-resource-factory). |
| 87 | |
| 88 | ## Feature flags: `Cloudflare.FlagshipApp` |
| 89 | |
| 90 | Cloudflare's Flagship feature flags land as full resources with an |
| 91 | Effect-native Worker binding |
| 92 | ([#602](https://github.com/alchemy-run/alchemy/pull/602)). |
| 93 | Declare the app and its flags: |
| 94 | |
| 95 | ```typescript |
| 96 | const app = yield* Cloudflare.FlagshipApp("Flags", {}); |
| 97 | |
| 98 | yield* Cloudflare.FlagshipFlag("NewCheckout", { |
| 99 | appId: app.appId, |
| 100 | key: "new-checkout", |
| 101 | defaultVariation: "off", |
| 102 | variations: { off: false, on: true }, |
| 103 | }); |
| 104 | ``` |
| 105 | |
| 106 | `FlagshipApp.bind(app)` attaches the binding to the surrounding Worker |
| 107 | and returns a runtime client for evaluating flags: |
| 108 | |
| 109 | ```typescript |
| 110 | export const App = Cloudflare.FlagshipApp("Flags", {}); |
| 111 | |
| 112 | export default Cloudflare.Worker( |
| 113 | "FlagsWorker", |
| 114 | { main: import.meta.filename }, |
| 115 | Effect.gen(function* () { |
| 116 | const flags = yield* Cloudflare.FlagshipApp.bind(App); |
| 117 | return { |
| 118 | fetch: Effect.gen(function* () { |
| 119 | const enabled = yield* flags.getBooleanValue("new-checkout", false, { |
| 120 | userId: "user-42", |
| 121 | }); |
| 122 | return HttpServerResponse.text(enabled ? "on" : "off"); |
| 123 | }), |
| 124 | }; |
| 125 | }).pipe(Effect.provide(Cloudflare.FlagshipBindingLive)), |
| 126 | ); |
| 127 | ``` |
| 128 | |
| 129 | Every method of the runtime `Flagship` binding is mirrored as an |
| 130 | Effect. Async Workers can bind the app on `env` instead and use the |
| 131 | raw binding. |
| 132 | |
| 133 | ## `Provider.list` and `alchemy unsafe nuke` |
| 134 | |
| 135 | Providers can now implement a `list()` lifecycle operation that |
| 136 | enumerates every live resource of their type in the ambient |
| 137 | account/zone |
| 138 | ([#620](https://github.com/alchemy-run/alchemy/pull/620)) — |
| 139 | each item returned in the same `Attributes` shape `read` produces, so |
| 140 | it's directly deletable. |
| 141 | |
| 142 | Built on top of it: `alchemy unsafe nuke` |
| 143 | ([#624](https://github.com/alchemy-run/alchemy/pull/624)) |
| 144 | enumerates everything a profile's providers can see and deletes it |
| 145 | all. With 230 resource types now enumerable, a burned test account is |
| 146 | a one-liner to clean: |
| 147 | |
| 148 | ```sh |
| 149 | alchemy unsafe nuke \ |
| 150 | --include "Cloudflare.*" \ |
| 151 | --filter 'resource.workerName === "alchemy-state-store"' \ |
| 152 | --dry-run |
| 153 | ``` |
| 154 | |
| 155 | `--include` / `--exclude` take provider-ID globs; `--filter` takes |
| 156 | JavaScript expressions evaluated with `resource` in scope — any |
| 157 | resource for which an expression is truthy is **spared**. `--dry-run` |
| 158 | previews, `--yes` skips the confirmation. The command is deliberately |
| 159 | hidden from `--help`; it deletes real infrastructure. |
| 160 | |
| 161 | ## GitHub repositories and webhook events |
| 162 | |
| 163 | **[Justin Bennett](https://github.com/just-be-dev)** contributed |
| 164 | `GitHub.Repository` |
| 165 | ([#607](https://github.com/alchemy-run/alchemy/pull/607)) — |
| 166 | full repository lifecycle with a converging reconciler that reads by |
| 167 | stable numeric id, so renaming a repo out from under the stack doesn't |
| 168 | corrupt state: |
| 169 | |
| 170 | ```typescript |
| 171 | const repo = yield* GitHub.Repository("internal-tools", { |
| 172 | owner: "my-org", |
| 173 | name: "internal-tools", |
| 174 | visibility: "private", |
| 175 | deleteBranchOnMerge: true, |
| 176 | }); |
| 177 | ``` |
| 178 | |
| 179 | On top of it, `GitHub.events` turns a repository into an **event |
| 180 | source** for Cloudflare Workers |
| 181 | ([#619](https://github.com/alchemy-run/alchemy/pull/619)). |
| 182 | Subscribing in a Worker's init provisions a `GitHub.Webhook` pointed |
| 183 | at the Worker's URL; at runtime, deliveries are verified with a |
| 184 | constant-time HMAC-SHA256 signature check and handed to your handler |
| 185 | as fully-typed payloads: |
| 186 | |
| 187 | ```typescript |
| 188 | const secret = yield* Config.redacted("GITHUB_WEBHOOK_SECRET"); |
| 189 | |
| 190 | // `event.name` is narrowed to "push" | "pull_request" |
| 191 | yield* GitHub.events({ |
| 192 | owner: "my-org", |
| 193 | repository: "my-repo", |
| 194 | events: ["push", "pull_request"], |
| 195 | secret, |
| 196 | }).subscribe((event) => |
| 197 | Effect.log(`received ${event.name} (${event.id})`), |
| 198 | ); |
| 199 | ``` |
| 200 | |
| 201 | ## S3-backed state store: `AWS.state()` |
| 202 | |
| 203 | AWS stacks get a first-class remote state backend, mirroring |
| 204 | `Cloudflare.state()` |
| 205 | ([#585](https://github.com/alchemy-run/alchemy/pull/585)): |
| 206 | |
| 207 | ```typescript |
| 208 | const Stack = Alchemy.Stack( |
| 209 | "my-stack", |
| 210 | { providers: AWS.providers(), state: AWS.state() }, |
| 211 | Effect.gen(function* () { |
| 212 | // ... |
| 213 | }), |
| 214 | ); |
| 215 | ``` |
| 216 | |
| 217 | State lives at `s3://{bucket}/{prefix}{stack}/{stage}/{fqn}.json`. By |
| 218 | default the bucket is an auto-created account-regional bucket |
| 219 | (`alchemy-state-{accountId}-{region}-an`); pass `bucketName` / |
| 220 | `prefix` to use your own. Credential resolution and bucket creation |
| 221 | are deferred to the first state operation, so nothing touches AWS at |
| 222 | layer construction. |
| 223 | |
| 224 | ## Also in this release |
| 225 | |
| 226 | - **`bundle: false` on `Cloudflare.Worker`** uploads a prebuilt `main` |
| 227 | byte-for-byte — no rolldown, no minification — with glob-based |
| 228 | `rules` selecting additional modules. The working path for OpenNext |
| 229 | and other externally-bundled outputs. Thanks |
| 230 | **[Alex](https://github.com/agcty)** |
| 231 | ([#592](https://github.com/alchemy-run/alchemy/pull/592)). |
| 232 | - **`alchemy plan` starts near-instant** — 8.2s → 1.8s warm, via |
| 233 | lazy-loading the TUI and vite plugin and deferring distilled schema |
| 234 | construction |
| 235 | ([#618](https://github.com/alchemy-run/alchemy/pull/618)). |
| 236 | - **`worker.url` is stable across deploys** when the first domain is |
| 237 | unchanged, so downstream resources referencing it (e.g. webhook |
| 238 | delivery URLs) stop phantom-updating |
| 239 | ([#616](https://github.com/alchemy-run/alchemy/pull/616)); |
| 240 | same for `worker.durableObjectNamespaces` when the DO class set is |
| 241 | unchanged |
| 242 | ([#617](https://github.com/alchemy-run/alchemy/pull/617)). |
| 243 | - **Lambda function URLs accept full config** — |
| 244 | `url: { authType: "AWS_IAM", cors, invokeMode: "RESPONSE_STREAM" }`; |
| 245 | IAM-auth URLs drop the public permission statement. Thanks |
| 246 | **[José Netto](https://github.com/mineiro)** |
| 247 | ([#614](https://github.com/alchemy-run/alchemy/pull/614)). |
| 248 | - **`alchemy dev` stays alive when an apply fails**, logging the full |
| 249 | cause instead of silently exiting. Thanks |
| 250 | **[Matthew Aylward](https://github.com/Butch78)** |
| 251 | ([#582](https://github.com/alchemy-run/alchemy/pull/582)). |
| 252 | - **StaticSite forwards `dev.env`** (including Redacted secrets) to |
| 253 | the external dev-server command. Thanks |
| 254 | **[Alex](https://github.com/agcty)** |
| 255 | ([#587](https://github.com/alchemy-run/alchemy/pull/587)). |
| 256 | - **Local assets binding no longer 500s** in `alchemy dev` |
| 257 | ([#613](https://github.com/alchemy-run/alchemy/pull/613)). |
| 258 | - **Drizzle drift detection is cached** per plan/apply cycle, so |
| 259 | drizzle-kit's interactive rename prompt appears once instead of |
| 260 | hanging `alchemy dev` |
| 261 | ([#597](https://github.com/alchemy-run/alchemy/pull/597)). |
| 262 | |
| 263 | ## Where to go next |
| 264 | |
| 265 | - [Looping the Generation of IaC and SDKs](/blog/2026-07-02-cloudflare-resource-factory) |
| 266 | - [Concepts › Providers](/infrastructure-as-code/provider) |
| 267 | - [Concepts › State Store](/state-store) |
| 268 | - [CLI guide](/cli) |
| 269 | - [Deploy a Lambda Function](/aws/compute/lambda) |
| 270 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta56) |
| 271 | - [Compare v2.0.0-beta.55 → v2.0.0-beta.56](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.55...v2.0.0-beta.56) |