Skip to content

2.0.0-beta.48 - Zones, Tunnels & Tokens

v2.0.0-beta.48 is a Cloudflare-heavy release: a managed Zone resource, runtime Tunnel bindings, a RateLimit binding, and a new create-token CLI command.

Manage a Cloudflare DNS zone as a resource:

const zone = yield* Cloudflare.Zone("MyZone", {
name: "example.com",
});

The full zone object is typed on the output — zone.zoneId, zone.nameServers, zone.status, and the rest.

This creates the zone if it doesn’t exist, and by default does not delete it on teardown. To import an existing zone, pipe adopt():

import * as AdoptPolicy from "alchemy/AdoptPolicy";
yield* Cloudflare.Zone("MyZone", { name: "example.com" }).pipe(
AdoptPolicy.adopt(),
);

To have it deleted on teardown, pipe destroy():

import * as RemovalPolicy from "alchemy/RemovalPolicy";
yield* Cloudflare.Zone("MyZone", { name: "example.com" }).pipe(
RemovalPolicy.destroy(),
);

This also adds Resource(type, { defaultRemovalPolicy }) so a resource type can flip the engine’s destroy-by-default (Zone uses "retain"), and destroy() / retain() now default to true when called with no argument. (#493)

Bind a typed Tunnel client into a Worker and manage Cloudflare Tunnels at runtime. Bind it once in Init, then call methods from your handler:

Effect.gen(function* () {
const tunnels = yield* Cloudflare.TunnelReadWrite.bind();
return {
fetch: Effect.gen(function* () {
const tunnel = yield* tunnels.create({ name: "on-demand-tunnel" });
yield* tunnels.putConfiguration(tunnel.id!, {
ingress: [
{ hostname: "app.example.com", service: "http://localhost:3000" },
{ service: "http_status:404" },
],
});
const token = yield* tunnels.getToken(tunnel.id!);
return HttpServerResponse.json({ id: tunnel.id, token });
}),
};
}).pipe(Effect.provide(Cloudflare.TunnelReadWriteLive));

Pick the surface you need:

  • Cloudflare.TunnelReadget, list, getToken, getConfiguration
  • Cloudflare.TunnelWritecreate, update, delete, putConfiguration
  • Cloudflare.TunnelReadWrite — the full CRUD surface

Cloudflare.RateLimit({...}) counts arbitrary keys inside a Worker. The marker is dual-natured: a plain data structure in the env position and yieldable inside an effect-native Worker.

In an effect-native Worker, yielding it attaches the binding and returns the runtime client in one step:

Effect.gen(function* () {
const throttle = yield* Cloudflare.RateLimit({
namespaceId: 1001,
simple: { limit: 2, period: 10 },
});
return {
fetch: Effect.gen(function* () {
const { success } = yield* throttle.limit({ key: "ip" });
return HttpServerResponse.text(success ? "ok" : "rate limited");
}),
};
}).pipe(Effect.provide(Cloudflare.RateLimitBindingLive));

Or declare it on env for an async (non-Effect) Worker, where InferEnv maps it to the native cf.RateLimit:

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: {
THROTTLE: Cloudflare.RateLimit({
namespaceId: 1001,
simple: { limit: 2, period: 10 },
}),
},
});

Thanks Alex for the contribution. (#238)

Mint a Cloudflare API token from the terminal — e.g. to produce a CLOUDFLARE_API_TOKEN for CI or local deploys:

Terminal window
# curated permissions for typical Alchemy deploys
alchemy cloudflare create-token --name ci-token
# full-access "superuser" token (all permission groups)
alchemy cloudflare create-token --all-permissions --name admin

It’s standalone (no auth profile) and the token is never stored. Permission groups are resolved live from the account so IDs are always valid, and the minted token is verified via /user/tokens/verify. Because Cloudflare only grants permissions the authenticating credential holds, it authenticates with the account’s Global API Key (CLOUDFLARE_API_KEY / CLOUDFLARE_EMAIL, or prompted). See the CLI guide for the full command. (#496)

  • Cloudflare.BrowserRendering renamed to Cloudflare.Browser — along with the matching binding/client/error exports (#503).
  • Yieldable binding markersBrowser, Images, and DynamicWorkerLoader can be yield*ed directly inside an effect-native Worker to attach the binding and get the client in one step (#498, #501, #505).
  • Providers docs refocused — a new Bindings section and the Providers page now centers on the lifecycle interface (#502).
  • Zone leans on error tags — distilled bumped to 0.22.3 for the ZoneAlreadyExists tag, replacing brittle error-message matching; the same distilled bump widened generated enums to open unions, narrowed back at the API boundary across several providers (#507).