Skip to content

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

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.

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 Worker
const 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!

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

Website.Vinext supports vinext 1.0, including hybrid App and Pages Router builds (#2025). Thanks BlankParticle!

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.

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

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.

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.

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");

Docs: Turso · Tutorial.

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:

Terminal window
aws login --profile my-console
alchemy profile edit --add AWS --method console-login --set loginProfile=my-console

Thanks jperezrealini!

Docs: AWS setup.

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

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.

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.

  • QueueSink (#1781) drains an Effect Stream into 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 through ctx.exports.
  • R2 bucket locks (#1984) with locks on R2.Bucket, and R2.S3Credentials tokens 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) and get, 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.retention on Lambda Functions and ECS Tasks (#2042) and an opt-in recoveryWindowInDays on Secrets Manager secrets (#2053). Thanks TBIQ007!
  • Lambda timeouts are traced (#1494) — the root span ends and telemetry flushes timeoutMargin before the timeout.
  • AWS.providers() and AWS.state() use a provided AWSEnvironment (#1933). Thanks Bjorn Pagen!
  • GCP: IAM service accounts, project services, and private GKE (#1995), BigQuery views and external tables (#2082), and in-place databaseVersion upgrades 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 ordered envFiles (#1946, #1947), Podman (#2095), and authenticated image builds (#1858). Thanks bjorntechTobbe, armancharan, and EthanShoeDev!
  • Railway.Service delete only removes its own environment’s instance (#2184).
  • SvelteKit 3.0 (#2060) and a faster CLI with a simpler launcher (#2151). Thanks BlankParticle!
  • alchemy dev on AWS no longer hangs when Docker can’t publish an emulator port (#2215, #2217).