Skip to content

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 + Access
const 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,
});

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:

distilled/packages/cloudflare/patches/turnstile/getWidget.json
{
"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.

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.

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:

Terminal window
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.

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})`),
);

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.

  • bundle: false on Cloudflare.Worker uploads a prebuilt main byte-for-byte — no rolldown, no minification — with glob-based rules selecting additional modules. The working path for OpenNext and other externally-bundled outputs. Thanks Alex (#592).
  • alchemy plan starts near-instant — 8.2s → 1.8s warm, via lazy-loading the TUI and vite plugin and deferring distilled schema construction (#618).
  • worker.url is stable across deploys when the first domain is unchanged, so downstream resources referencing it (e.g. webhook delivery URLs) stop phantom-updating (#616); same for worker.durableObjectNamespaces when the DO class set is unchanged (#617).
  • Lambda function URLs accept full configurl: { authType: "AWS_IAM", cors, invokeMode: "RESPONSE_STREAM" }; IAM-auth URLs drop the public permission statement. Thanks José Netto (#614).
  • alchemy dev stays 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).