| 1 | --- |
| 2 | title: 2.0.0-beta.48 - Zones, Tunnels & Tokens |
| 3 | date: 2026-06-01T09:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.48 adds a `Cloudflare.Zone.Zone` resource, runtime Tunnel bindings, a `RateLimit` binding, and an `alchemy cloudflare create-token` CLI command for minting scoped API tokens. |
| 6 | --- |
| 7 | |
| 8 | `v2.0.0-beta.48` is a Cloudflare-heavy release: a managed |
| 9 | **`Zone`** resource, runtime **Tunnel** bindings, a **`RateLimit`** |
| 10 | binding, and a new **`create-token`** CLI command. |
| 11 | |
| 12 | ## `Cloudflare.Zone.Zone` |
| 13 | |
| 14 | Manage a Cloudflare DNS zone as a resource: |
| 15 | |
| 16 | ```typescript |
| 17 | const zone = yield* Cloudflare.Zone.Zone("MyZone", { |
| 18 | name: "example.com", |
| 19 | }); |
| 20 | ``` |
| 21 | |
| 22 | The full zone object is typed on the output — `zone.zoneId`, |
| 23 | `zone.nameServers`, `zone.status`, and the rest. |
| 24 | |
| 25 | This creates the zone if it doesn't exist, and by default does |
| 26 | **not** delete it on teardown. To import an existing zone, pipe |
| 27 | `adopt()`: |
| 28 | |
| 29 | ```typescript |
| 30 | import * as AdoptPolicy from "alchemy/AdoptPolicy"; |
| 31 | |
| 32 | yield* Cloudflare.Zone.Zone("MyZone", { name: "example.com" }).pipe( |
| 33 | AdoptPolicy.adopt(), |
| 34 | ); |
| 35 | ``` |
| 36 | |
| 37 | To have it deleted on teardown, pipe `destroy()`: |
| 38 | |
| 39 | ```typescript |
| 40 | import * as RemovalPolicy from "alchemy/RemovalPolicy"; |
| 41 | |
| 42 | yield* Cloudflare.Zone.Zone("MyZone", { name: "example.com" }).pipe( |
| 43 | RemovalPolicy.destroy(), |
| 44 | ); |
| 45 | ``` |
| 46 | |
| 47 | This also adds `Resource(type, { defaultRemovalPolicy })` so a |
| 48 | resource type can flip the engine's destroy-by-default (`Zone` |
| 49 | uses `"retain"`), and `destroy()` / `retain()` now default to |
| 50 | `true` when called with no argument. |
| 51 | ([#493](https://github.com/alchemy-run/alchemy/pull/493)) |
| 52 | |
| 53 | ## Tunnel bindings |
| 54 | |
| 55 | Bind a typed Tunnel client into a Worker and manage Cloudflare |
| 56 | Tunnels at runtime. Bind it once in Init, then call methods from |
| 57 | your handler: |
| 58 | |
| 59 | ```typescript |
| 60 | Effect.gen(function* () { |
| 61 | const tunnels = yield* Cloudflare.TunnelReadWrite.bind(); |
| 62 | |
| 63 | return { |
| 64 | fetch: Effect.gen(function* () { |
| 65 | const tunnel = yield* tunnels.create({ name: "on-demand-tunnel" }); |
| 66 | yield* tunnels.putConfiguration(tunnel.id!, { |
| 67 | ingress: [ |
| 68 | { hostname: "app.example.com", service: "http://localhost:3000" }, |
| 69 | { service: "http_status:404" }, |
| 70 | ], |
| 71 | }); |
| 72 | const token = yield* tunnels.getToken(tunnel.id!); |
| 73 | return HttpServerResponse.json({ id: tunnel.id, token }); |
| 74 | }), |
| 75 | }; |
| 76 | }).pipe(Effect.provide(Cloudflare.TunnelReadWriteLive)); |
| 77 | ``` |
| 78 | |
| 79 | Pick the surface you need: |
| 80 | |
| 81 | - `Cloudflare.Tunnel.ReadTunnel` — `get`, `list`, `getToken`, `getConfiguration` |
| 82 | - `Cloudflare.Tunnel.WriteTunnel` — `create`, `update`, `delete`, `putConfiguration` |
| 83 | - `Cloudflare.Tunnel.ReadWriteTunnel` — the full CRUD surface |
| 84 | |
| 85 | :::note[Under the covers] |
| 86 | Binding a Tunnel client creates a scoped `AccountApiToken` for |
| 87 | the Worker, attaches an allow policy with exactly the permission |
| 88 | groups that surface needs, and binds the token's value into the |
| 89 | Worker as a `secret_text`. The runtime client reads it back and |
| 90 | makes the `cfd_tunnel` API calls for you. Roughly, the binding |
| 91 | does this on your behalf: |
| 92 | |
| 93 | ```typescript |
| 94 | const token = yield* Cloudflare.ApiToken.AccountApiToken(`${worker}Token`); |
| 95 | |
| 96 | // attach exactly the permission groups for the chosen surface |
| 97 | yield* token.bind("tunnel", { |
| 98 | policies: [ |
| 99 | { |
| 100 | effect: "allow", |
| 101 | permissionGroups, // read scopes for TunnelRead, write for TunnelWrite |
| 102 | resources: { [`com.cloudflare.api.account.${accountId}`]: "*" }, |
| 103 | }, |
| 104 | ], |
| 105 | }); |
| 106 | |
| 107 | // inject token.value as secret_text; the client reads it at runtime |
| 108 | ``` |
| 109 | ::: |
| 110 | |
| 111 | ## `RateLimit` binding |
| 112 | |
| 113 | `Cloudflare.RateLimit({...})` counts arbitrary keys inside a |
| 114 | Worker. The marker is dual-natured: a plain data structure in the |
| 115 | `env` position **and** yieldable inside an effect-native Worker. |
| 116 | |
| 117 | In an effect-native Worker, yielding it attaches the binding |
| 118 | **and** returns the runtime client in one step: |
| 119 | |
| 120 | ```typescript |
| 121 | Effect.gen(function* () { |
| 122 | const throttle = yield* Cloudflare.RateLimit({ |
| 123 | namespaceId: 1001, |
| 124 | simple: { limit: 2, period: 10 }, |
| 125 | }); |
| 126 | |
| 127 | return { |
| 128 | fetch: Effect.gen(function* () { |
| 129 | const { success } = yield* throttle.limit({ key: "ip" }); |
| 130 | return HttpServerResponse.text(success ? "ok" : "rate limited"); |
| 131 | }), |
| 132 | }; |
| 133 | }).pipe(Effect.provide(Cloudflare.RateLimitBindingLive)); |
| 134 | ``` |
| 135 | |
| 136 | Or declare it on `env` for an async (non-Effect) Worker, where |
| 137 | `InferEnv` maps it to the native `cf.RateLimit`: |
| 138 | |
| 139 | ```typescript |
| 140 | export const Worker = Cloudflare.Worker("Worker", { |
| 141 | main: "./src/worker.ts", |
| 142 | env: { |
| 143 | THROTTLE: Cloudflare.RateLimit({ |
| 144 | namespaceId: 1001, |
| 145 | simple: { limit: 2, period: 10 }, |
| 146 | }), |
| 147 | }, |
| 148 | }); |
| 149 | ``` |
| 150 | |
| 151 | Thanks **[Alex](https://github.com/agcty)** for the |
| 152 | contribution. |
| 153 | ([#238](https://github.com/alchemy-run/alchemy/pull/238)) |
| 154 | |
| 155 | ## `alchemy cloudflare create-token` |
| 156 | |
| 157 | Mint a Cloudflare API token from the terminal — e.g. to produce a |
| 158 | `CLOUDFLARE_API_TOKEN` for CI or local deploys: |
| 159 | |
| 160 | ```sh |
| 161 | # curated permissions for typical Alchemy deploys |
| 162 | alchemy cloudflare create-token --name ci-token |
| 163 | |
| 164 | # full-access "superuser" token (all permission groups) |
| 165 | alchemy cloudflare create-token --all-permissions --name admin |
| 166 | ``` |
| 167 | |
| 168 | It's standalone (no auth profile) and the token is never stored. |
| 169 | Permission groups are resolved **live** from the account so IDs |
| 170 | are always valid, and the minted token is verified via |
| 171 | `/user/tokens/verify`. Because Cloudflare only grants permissions |
| 172 | the authenticating credential holds, it authenticates with the |
| 173 | account's Global API Key (`CLOUDFLARE_API_KEY` / `CLOUDFLARE_EMAIL`, |
| 174 | or prompted). See the |
| 175 | [CLI guide](/cli/cloudflare) for the full command. |
| 176 | ([#496](https://github.com/alchemy-run/alchemy/pull/496)) |
| 177 | |
| 178 | ## Also in this release |
| 179 | |
| 180 | - **`Cloudflare.BrowserRendering` renamed to `Cloudflare.Browser`** — |
| 181 | along with the matching binding/client/error exports |
| 182 | ([#503](https://github.com/alchemy-run/alchemy/pull/503)). |
| 183 | - **Yieldable binding markers** — `Browser`, `Images`, and |
| 184 | `DynamicWorkerLoader` can be `yield*`ed directly inside an |
| 185 | effect-native Worker to attach the binding and get the client in |
| 186 | one step |
| 187 | ([#498](https://github.com/alchemy-run/alchemy/pull/498), |
| 188 | [#501](https://github.com/alchemy-run/alchemy/pull/501), |
| 189 | [#505](https://github.com/alchemy-run/alchemy/pull/505)). |
| 190 | - **Providers docs refocused** — a new Bindings section and the |
| 191 | Providers page now centers on the lifecycle interface |
| 192 | ([#502](https://github.com/alchemy-run/alchemy/pull/502)). |
| 193 | - **`Zone` leans on error tags** — distilled bumped to `0.22.3` |
| 194 | for the `ZoneAlreadyExists` tag, replacing brittle |
| 195 | error-message matching; the same distilled bump widened |
| 196 | generated enums to open unions, narrowed back at the API |
| 197 | boundary across several providers |
| 198 | ([#507](https://github.com/alchemy-run/alchemy/pull/507)). |
| 199 | |
| 200 | ## Where to go next |
| 201 | |
| 202 | - [CLI guide](/cli) |
| 203 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta48) |
| 204 | - [Compare v2.0.0-beta.47 → v2.0.0-beta.48](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.47...v2.0.0-beta.48) |