2.0.0-beta.82 - Cloudflare Birthday Week
beta.82 supports Cloudflare’s Birthday Week launches. Containers can now be managed by Durable Objects, which pick the image and size when they start one. K2 gives you durable event streams, and Basin turns Pipelines and the R2 Data Catalog into one data platform. Workers KV Instant, Workers Issues, and vinext 1.0 work out of the box.
Outside Birthday Week, Workers can reach private databases through
Hyperdrive and Workers VPC, set Workers Cache per entrypoint, and
serve one hostname from many Workers with federated APIs, and take over
existing Cloudflare Access apps. Durable
Objects now deploy with Cloudflare’s declarative exports. Kubernetes resources send Redacted
secrets to the cluster. There is also a new Turso provider, AWS console login, DSQL migrations,
and Neon.Role.
beta.82 also ships 93 bug fixes and closes 51 issues. See Bug fixes.
Cloudflare Birthday Week
Section titled “Cloudflare Birthday Week”Containers managed by Durable Objects
Section titled “Containers managed by Durable Objects”Cloudflare rebuilt Containers so a Durable Object can start one directly.
Set schedulingPolicy: "durable_object" and declare named images
(#1905). The Durable
Object chooses the image, instance size, and entrypoint when it calls
start(), then runs commands with exec:
export class Sandbox extends Cloudflare.Container<Sandbox>()("Sandbox", { schedulingPolicy: "durable_object", images: { node: { image: "node:24-slim" } },}) {}
export class Agent extends Cloudflare.DurableObject<Agent>()( "Agent", Effect.gen(function* () { const sandbox = yield* Sandbox; return Effect.succeed({ exec: (args: string[]) => Effect.gen(function* () { const images = yield* sandbox.images; yield* sandbox.start({ image: images.node, instance: "lite", entrypoint: ["sleep", "infinity"], enableInternet: false, }); const process = yield* sandbox.exec(args); const result = yield* process.output(); return new TextDecoder().decode(result.stdout); }).pipe(Effect.scoped), }); }),) {}Snapshots, SSH, and stdin streaming are supported, and the same code runs
under alchemy dev. Thanks
Dan van der Merwe!
Docs: Containers · Container lifecycle.
K2 event streams
Section titled “K2 event streams”Cloudflare.K2.Stream is a durable event stream stored in R2
(#2010). A Worker
writes to it through a binding:
export const Orders = Cloudflare.K2.Stream("Orders", { retention: "7 days",});
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const orders = yield* Cloudflare.K2.WriteStream(Orders); return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; yield* orders.send([{ content: yield* request.text }]); return HttpServerResponse.empty({ status: 202 }); }), }; }).pipe(Effect.provide(Cloudflare.K2.WriteStreamBinding)),) {}Long-running hosts consume the stream with consumeStreamRecords. Here a
Fly Service reviews large orders:
yield* Cloudflare.K2.consumeStreamRecords( Orders, { startAt: "earliest", concurrency: 4 }, (records) => records.pipe( Stream.map((record) => record.value), Stream.filter((order) => order.amountCents > 100_000), Stream.runForEach((order) => Effect.log(`review ${order.orderId}`)), ),);Docs: K2.
Cloudflare renamed its data platform Basin: Pipelines, the R2 Data
Catalog, and SQL. The same resources are now also exported as
Cloudflare.Basin.* (#2010).
A pipeline reads a stream and writes to an Iceberg table with SQL:
const lake = yield* Cloudflare.R2.Bucket("Lake");const catalog = yield* Cloudflare.Basin.Catalog("Catalog", { bucket: lake });const views = yield* Cloudflare.Basin.Stream("PageViews", { schema: PageView });
const table = yield* Cloudflare.Basin.Sink("PageViewTable", { type: "basin_catalog", catalog, table: { namespace: "web", name: "page_views" }, token: yield* Config.Redacted("CATALOG_TOKEN"),});
yield* Cloudflare.Basin.Pipeline("PageViewEtl", { sql: Output.interpolate` INSERT INTO ${table.name} SELECT userId, path, at FROM ${views.name}`,});Alchemy checks the SQL with Cloudflare during plan, so a bad statement fails before anything changes.
Basin.Table creates an Iceberg table directly, with columns from an Effect
Schema:
const orders = yield* Cloudflare.Basin.Table("Orders", { catalog, namespace: "sales", schema: Order, partitionBy: ["day(at)"],});Docs: Pipelines · Iceberg tables.
Workers KV Instant
Section titled “Workers KV Instant”Cloudflare.KV.InstantNamespace creates a namespace in Cloudflare’s new
Instant mode, for small configuration that is read often
(#2027). The existing
KV capabilities accept it, and alchemy dev emulates it:
const config = yield* Cloudflare.KV.InstantNamespace("Config");
// in a Workerconst kv = yield* Cloudflare.KV.ReadNamespace(config);const enabled = yield* kv.get("feature-enabled");Switching a namespace between classic and Instant replaces it and does not copy its data. Thanks BlankParticle!
Workers Issues
Section titled “Workers Issues”Turn on Workers Issues error detection for a Worker (#1992). Version uploads and gradual rollouts keep the setting:
yield* Cloudflare.Worker("Api", { main: "./src/worker.ts", observability: { issues: { enabled: true } },});vinext 1.0
Section titled “vinext 1.0”Website.Vinext supports vinext 1.0, including hybrid App and Pages
Router builds (#2025).
Thanks BlankParticle!
Private databases with Hyperdrive
Section titled “Private databases with Hyperdrive”A Hyperdrive connection can now reach a database that has no public
endpoint, through a Workers VPC service
(#2218). Point the
origin at the service with serviceId:
const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("Hyperdrive", { origin: { scheme: "postgres", serviceId: service.serviceId, database: "app", user: "app", password: password.text, },});The guide connects a Worker to Aurora in private AWS subnets through a
Cloudflare Tunnel, and examples/cloudflare-hyperdrive-aws-vpc deploys
the whole thing.
Docs: Private databases.
Workers Cache per entrypoint
Section titled “Workers Cache per entrypoint”entrypoints sets Workers Cache for each entrypoint of a Worker
(#2216). An
entrypoint you don’t list uses the Worker’s cache setting:
yield* Cloudflare.Worker("Api", { main: "./src/worker.ts", entrypoints: { default: { cache: { enabled: false } }, Public: { cache: { enabled: true } }, },});Durable Objects deploy with exports
Section titled “Durable Objects deploy with exports”Alchemy now uploads Durable Objects with Cloudflare’s declarative
exports instead of migrations
(#2135). Your code
doesn’t change. Alchemy can now deploy and adopt Workers that were last
deployed by Wrangler 4.107 or later.
The first deploy after upgrading must go out at 100% traffic. If you use
gradual deployments, remove version.traffic for that one deploy;
otherwise it fails with WorkerVersionConfigError.
Removing a Durable Object binding no longer deletes its namespace while
the class is still exported. Remove the class from your code to delete
it. Previews still use migrations.
Docs: Durable Objects · Gradual deployments.
Federated APIs
Section titled “Federated APIs”Serve one hostname from several Workers, each with only the permissions
it needs, using zone routes
(#2088). alchemy dev
applies the same routing locally:
yield* Cloudflare.Worker("Home", { main: "./src/home.ts", domain: "api.example.com",});
yield* Cloudflare.Worker("Orders", { main: "./src/orders.ts", routes: [{ pattern: "api.example.com/orders*" }],});Docs: Federated APIs.
Cloudflare Access: adopt apps and SaaS
Section titled “Cloudflare Access: adopt apps and SaaS”Access.Application can take over an existing app by its Cloudflare id
(#1848). The id and
aud stay the same, so JWT validation keeps working, and no --adopt is
needed:
const grafana = yield* Cloudflare.Access.Application("Grafana", { applicationId: "4f8e3c1a-5b2d-4e6f-9a7b-1c2d3e4f5a6b", type: "self_hosted", domain: "grafana.example.com",});If the id doesn’t exist, the deploy fails with DeclaredApplicationNotFound
instead of creating a new app.
SAML and OIDC SaaS apps are supported too. The values Cloudflare generates, such as the SSO endpoint and client id, come back as outputs:
const looker = yield* Cloudflare.Access.Application("Looker", { type: "saas", saasApp: { authType: "saml", spEntityId: "https://example.looker.com", consumerServiceUrl: "https://example.looker.com/saml/acs", },});Access.Policy gains SSH and RDP connection rules, approval groups, and a
justification prompt:
yield* Cloudflare.Access.Policy("SshOps", { decision: "allow", include: [{ emailDomain: "example.com" }], connectionRules: { ssh: { usernames: ["root", "ubuntu"] } }, approvalGroups: [{ approvalsNeeded: 1, emailAddresses: ["lead@example.com"] }], purposeJustificationRequired: true,});Updates no longer reset settings you leave out of the props. A field you
omit keeps its live value; pass [] or false to clear it. Thanks
spinx and
Chinoman10!
Docs: Access.
A new Turso provider creates groups and databases, with migrations, and binds them to Workers and Lambdas (#2138):
import * as Turso from "alchemy/Turso";
export const Group = Turso.Group("Group", { location: "aws-us-east-1" });
export const Db = Effect.gen(function* () { const group = yield* Group; return yield* Turso.Database("Db", { group: group.name, migrations: "./migrations", });});Query it with Drizzle or SQL.LibSQL:
const conn = yield* Turso.Connect(Db);const db = yield* Drizzle.LibSQL(conn);Turso.ManageDatabases creates a database per tenant at runtime:
const tenants = yield* Turso.ManageDatabases(Tenants);const acme = yield* tenants.create("acme");AWS console login
Section titled “AWS console login”Authenticate an AWS profile with aws login (AWS CLI 2.32+), with no
long-lived keys
(#1759).
alchemy profile refresh signs in again when the session expires:
aws login --profile my-consolealchemy profile edit --add AWS --method console-login --set loginProfile=my-consoleThanks jperezrealini!
Docs: AWS setup.
DSQL migrations
Section titled “DSQL migrations”AWS.DSQL.Cluster applies SQL migrations when it deploys
(#1544). Each
statement runs in its own transaction, and CREATE INDEX becomes
CREATE INDEX ASYNC. SQL that DSQL can’t run is rejected before the
cluster is touched:
const database = yield* AWS.DSQL.Cluster("Database", { migrations: "./migrations",});Thanks carlulsoe!
Docs: Drizzle on DSQL.
Neon roles
Section titled “Neon roles”Neon.Role creates a Postgres role on a branch, with a stable password
(#2078):
const app = yield* Neon.Role("App", { branch });const owner = yield* Neon.Role("Owner", { branch, name: "app_owner", noLogin: true });Every Neon role has owner-level access to the branch’s databases.
Docs: Roles.
Kubernetes secrets and drift
Section titled “Kubernetes secrets and drift”Redacted values in a Kubernetes.Manifest or Kubernetes.HelmChart
now reach the cluster as the real secret
(#1948). Before, the
cluster stored the placeholder <redacted>. Plans and state still keep
the value wrapped:
yield* Kubernetes.Manifest("Token", { cluster, manifest: { apiVersion: "v1", kind: "Secret", metadata: { name: "token" }, stringData: { token: Redacted.make(token) }, },});
yield* Kubernetes.HelmChart("App", { cluster, chart: "./chart", values: { apiKey: Redacted.make(apiKey) },});API errors and Helm output are scrubbed of those secrets, including their
base64 and JSON-escaped forms. Destroy fails when the cluster refuses a
delete (403 or 5xx) instead of reporting success, and calls to an
unreachable API server time out. alchemy drift now reports in-cluster
edits to the fields you declared, such as an image or replica count.
Kubernetes.Deployment also takes the cluster resource directly, so its
props can be a plain object
(#2005):
export default Kubernetes.Deployment( "Api", Effect.gen(function* () { return { main: import.meta.url, cluster: yield* Gke, port: 3000 }; }), { main: import.meta.url, cluster: Gke, port: 3000 }, impl,);Neon.Function takes branch and project the same way.
Docs: Kubernetes.
93 bug fixes
Section titled “93 bug fixes”This release merges 93 fixes: 29 for Cloudflare and its local runtime, 23 for AWS, 8 for Docker, and the rest across PlanetScale, the CLI, Prisma, GCP, frontend frameworks, Neon, floci, and more.
One of them, #2174, fixes the resource leaks, slow deletes, and flaky tests that kept the live AWS, Cloudflare, and GCP suites from passing with clean accounts. For example, deleting a Lambda function no longer waits about 40 seconds for its log group, interrupted creates on CloudFront and Vertex AI are cleaned up instead of leaked, and a replaced ACM PCA certificate authority no longer keeps its old policy.
The full list is in the CHANGELOG.
Also in this release
Section titled “Also in this release”QueueSink(#1781) drains an EffectStreaminto a Queue, in batches Cloudflare accepts.- Bind a Worker’s own entrypoints
(#2115,
#2161) —
Cloudflare.WorkerEntrypoint<Api>("Api")without a Worker targets the Worker that declares it, and Effect Workers call themselves throughctx.exports. - R2 bucket locks
(#1984) with
locksonR2.Bucket, andR2.S3Credentialstokens are scoped to their bucket (#1985) instead of every bucket in the account. Thanks 0x0f0f0f! - AI crawler policies on
BotManagement(#1911),Ssl.CertificateTransparencyAlerting(#1913), and email sending events on Queues (#1914). Thanks birbprophet! MtlsCertificate.Fetch(#2051) calls an origin with a client certificate from an Effect Worker. Thanks TBIQ007!- Durable Object namespaces support
jurisdiction(#2007) andget,idFromName,idFromString,newUniqueId(#852). Thanks Makisuo and Butch78! - Workers AI reports billed neurons on finish parts (#1724), and streamed text, tool calls, and reasoning are fixed (#2064).
- DynamoDB HTTP bindings from Cloudflare Workers (#1782).
logging.retentionon Lambda Functions and ECS Tasks (#2042) and an opt-inrecoveryWindowInDayson Secrets Manager secrets (#2053). Thanks TBIQ007!- Lambda timeouts are traced
(#1494) — the root
span ends and telemetry flushes
timeoutMarginbefore the timeout. AWS.providers()andAWS.state()use a providedAWSEnvironment(#1933). Thanks Bjorn Pagen!- GCP: IAM service accounts, project services, and private GKE
(#1995), BigQuery
views and external tables
(#2082), and
in-place
databaseVersionupgrades on Cloud SQL (#2029). Thanks connyay and bartek671! - PlanetScale
deletionProtection(#1861) — thanks agcty! — and an edited migration that already ran now replaces a dev branch or fails on production (#1489). - Docker runtime options (
networkMode,capAdd,devices) and orderedenvFiles(#1946, #1947), Podman (#2095), and authenticated image builds (#1858). Thanks bjorntechTobbe, armancharan, and EthanShoeDev! Railway.Servicedelete only removes its own environment’s instance (#2184).- SvelteKit 3.0 (#2060) and a faster CLI with a simpler launcher (#2151). Thanks BlankParticle!
alchemy devon AWS no longer hangs when Docker can’t publish an emulator port (#2215, #2217).