2.0.0-beta.64 - Full AWS Coverage
beta.64 is the AWS release. Alchemy now covers 206 AWS services, with every resource and binding live-tested against the real cloud. It also adds first-class containers on ECS and EKS, Durable Lambda Functions, and Helm charts on EKS.
“100%” AWS coverage
Section titled ““100%” AWS coverage”One PR (#797) took the AWS provider from a handful of services to all of them, using the same resource factory that built out the Cloudflare provider. Fleets of agents cataloged all 428 generated SDK modules, then implemented and live-tested them in waves: serverless first, then ECS/Fargate, EKS, EC2, and the long tail. Everything left over is documented as out of scope (deprecated APIs, data-only endpoints, billing objects). The result:
- 206 AWS services with 683 resources and 2,842 bindings (plus event sources and sinks), from DynamoDB and S3 all the way out to MediaLive, IoT SiteWise, Bedrock, and SageMaker.
- 2,692 live tests green by default against real AWS. 375 more are gated behind env vars because they are slow to provision, expensive, or need special account entitlements.
- Zero-orphan guarantee: each round wiped the test account clean, ran the full suite, then audited the account for leftover resources. The loop ran until two consecutive rounds left nothing behind. A passing test that leaks a cloud resource is a provider bug, and the loop caught dozens of them.
Bindings are 1:1 with IAM actions. Each binding your code uses becomes a least-privilege policy statement on exactly the resources it touches, whether the code runs on Lambda, ECS, or EKS:
const api = yield* AWS.ECS.Service( "Api", { cluster, main: import.meta.url, port: 3000, desiredCount: 2 }, Effect.gen(function* () { // init: each binding grants one IAM action on one ARN const getItem = yield* AWS.DynamoDB.GetItem(table); const send = yield* AWS.SQS.SendMessage(queue);
return { fetch: Effect.gen(function* () { const item = yield* getItem({ Key: { pk: { S: "user#1" } } }); yield* send({ MessageBody: JSON.stringify(item.Item) }); return yield* HttpServerResponse.json({ ok: true }); }), }; }).pipe( Effect.provide([AWS.DynamoDB.GetItemHttp, AWS.SQS.SendMessageHttp]), ),);You never write an IAM policy, and every error is typed — the compiler tells you what can fail and you handle it like any other Effect error. See Bindings for how it works.
All of it is documented in the API reference, with usage examples on every resource and binding.
ECS: Tasks and Services
Section titled “ECS: Tasks and Services”ECS gets the full platform treatment
(#867):
Task for run-to-completion work and
Service for long-running servers.
Both are authored the same three ways as Workers and Lambda: an
external image, an inline effect, or a tagged class.
Every container platform takes one image source, flat on props:
{ main } // bundle this Effect program{ main, image } // bundle FROM your base image{ context, dockerfile? } // build your own Dockerfile{ dockerfile: Dockerfile.inline`...` } // inline Dockerfile content{ image } // registry ref, mirrored into ECRThe simplest Service runs a registry image behind a provisioned
Application Load Balancer:
const cluster = yield* AWS.ECS.Cluster("AppCluster", {});
const nginx = yield* AWS.ECS.Service("Edge", { cluster, image: "public.ecr.aws/nginx/nginx:1.27", port: 80, desiredCount: 2, loadBalancer: true,});
return { url: nginx.url }; // http://<alb-dns-name>A Service can also run an Effect program. The impl returns
{ fetch }, and bindings grant IAM to the task role:
const api = yield* AWS.ECS.Service( "Api", { cluster, main: import.meta.url, port: 3000, desiredCount: 2 }, Effect.gen(function* () { const putItem = yield* AWS.DynamoDB.PutItem(table); return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const order = (yield* request.json) as { orderId: string }; yield* putItem({ Item: { pk: { S: order.orderId } } }); return yield* HttpServerResponse.json({ ok: true }); }), }; }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);A one-shot Task is the run-to-completion counterpart: the impl
returns { run }, and the container exits when the effect
completes. Here it composes main with Dockerfile.inline — the
inline content builds the environment, and the bundled program is
layered on top:
import { Dockerfile } from "alchemy/Docker";
const thumbnailer = yield* AWS.ECS.Task( "Thumbnailer", { main: import.meta.url, dockerfile: Dockerfile.inline` FROM oven/bun:1 RUN apt-get update && apt-get install -y ffmpeg `, cpu: 1024, memory: 2048, }, Effect.gen(function* () { const getObject = yield* AWS.S3.GetObject(bucket); return { run: Effect.gen(function* () { // pull the video from S3, shell out to ffmpeg, then exit }), }; }).pipe(Effect.provide(AWS.S3.GetObjectHttp)),);Tasks are launched from other compute with the
RunTask binding, which grants
ecs:RunTask and iam:PassRole automatically — here a Lambda
function launches a task per request:
const api = yield* AWS.Lambda.Function( "Api", { main: import.meta.url, url: true }, Effect.gen(function* () { const runTask = yield* AWS.ECS.RunTask(cluster, thumbnailer);
return { fetch: Effect.gen(function* () { const response = yield* runTask({ launchType: "FARGATE", networkConfiguration: { awsvpcConfiguration: { subnets: [subnetId] }, }, }); return yield* HttpServerResponse.json({ taskArn: response.tasks?.[0]?.taskArn, }); }), }; }),);Or run a task on a schedule with AWS.ECS.every, which
provisions the EventBridge rule and invoke role:
yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", { cluster, task: nightlyTask, subnets: [privateSubnet.subnetId],});Services go well past loadBalancer: true: multiple services can
share one load balancer, and props cover custom domains,
auto-scaling on CPU or request count, Fargate Spot capacity, EFS
volumes, secrets, and health checks. Docs:
ECS.
EKS: Deployments, Jobs, Manifests, and Helm
Section titled “EKS: Deployments, Jobs, Manifests, and Helm”Kubernetes workloads live in the same TypeScript program as the
cluster itself — no YAML, no kubectl. compute: "auto" stands
up an EKS Auto Mode cluster where AWS manages the nodes:
const network = yield* AWS.EC2.Network("Network", { cidrBlock: "10.42.0.0/16", availabilityZones: 2, nat: "single",});
const cluster = yield* AWS.EKS.Cluster("Cluster", { compute: "auto", resourcesVpcConfig: { subnetIds: network.privateSubnetIds },});
const api = yield* AWS.EKS.Deployment( "Api", { cluster, main: import.meta.url, port: 3000, replicas: 2, serviceType: "LoadBalancer" }, Effect.gen(function* () { const putItem = yield* AWS.DynamoDB.PutItem(table); // → pod-identity IAM return { fetch: Effect.gen(function* () { /* ... */ }) }; }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);Deployment synthesizes a
Kubernetes Deployment, Service, and ServiceAccount, with
bindings landing as pod-identity IAM.
Job is the run-to-completion
counterpart — same image sources, and the impl returns { run }:
const seed = yield* AWS.EKS.Job( "SeedData", { cluster, main: import.meta.url }, Effect.gen(function* () { const putItem = yield* AWS.DynamoDB.PutItem(table); return { run: Effect.gen(function* () { // runs to completion, then the pod exits }), }; }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);Add schedule and a Job becomes a Kubernetes CronJob — here in
the external form, running a pre-built image:
const nightly = yield* AWS.EKS.Job("NightlyBackfill", { cluster, image: "ghcr.io/acme/backfill:v3", schedule: "0 3 * * *",});Manifest applies any raw
Kubernetes object via server-side apply, CRDs included — the
manifest is a literal object, exactly what you’d write in YAML.
No kubeconfig required:
const sts = yield* AWS.EKS.Manifest("Cache", { cluster, manifest: { apiVersion: "apps/v1", kind: "StatefulSet", metadata: { name: "cache", namespace: "apps" }, spec: { replicas: 3 /* ... */ }, },});HelmChart installs Helm charts
(#891). It
renders the chart locally with helm template and applies the
objects to the cluster, so Alchemy owns the lifecycle: drift is
corrected on every deploy, dropped objects are pruned, and
destroy deletes everything:
const ingress = yield* AWS.EKS.HelmChart("IngressNginx", { cluster, chart: "ingress-nginx", repo: "https://kubernetes.github.io/ingress-nginx", version: "4.11.2", namespace: "ingress-nginx", createNamespace: true, values: { controller: { replicaCount: 2 } },});Docs: EKS.
Durable Lambda Functions
Section titled “Durable Lambda Functions”AWS.Lambda.DurableFunction writes long-running orchestrations
as plain code, built on AWS Lambda’s durable execution model
(#797). It
is a Lambda Function, with the same props and bindings, but
its body is checkpointed. Durable.step records each result so
it isn’t re-run on replay. Durable.sleep suspends the execution
entirely; Lambda re-invokes the function to resume it. And
Durable.waitForCallback pauses on an external signal for up to
a year:
export class OrderFlow extends AWS.Lambda.DurableFunction<OrderFlow>()( "OrderFlow",) {}
export default OrderFlow.make( { main: import.meta.url, executionTimeout: "1 hour" }, Effect.gen(function* () { // init: resolve typed binding clients (IAM lands on this role) const putItem = yield* AWS.DynamoDB.PutItem(table);
return Effect.fn(function* (input: { orderId: string }) { const reserved = yield* AWS.Lambda.Durable.step( "reserve", putItem({ Item: { pk: { S: input.orderId } } }).pipe(Effect.orDie), { retry: { limit: 3, delay: "5 seconds" } }, ); yield* AWS.Lambda.Durable.sleep("cooldown", "10 minutes"); return { orderId: input.orderId, reserved }; }); }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);Replay requires the body to be deterministic, and the type system
enforces it: the run body has no access to cloud credentials, so
the only way to do I/O is through a binding client resolved in
init and called inside a Durable.step.
yield* OrderFlow returns a typed handle to start, inspect, and
stop executions, and to complete callbacks from the outside:
const orders = yield* OrderFlow;const ref = yield* orders.start({ name: "order-123", // idempotent start params: { orderId: "123" },});const execution = yield* orders.get(ref.executionArn!);// execution.Status: "RUNNING" | "SUCCEEDED" | "FAILED" | ...The vocabulary mirrors Cloudflare Workflows, so orchestration code reads the same on both clouds. Reference: DurableFunction.
Effect SQL and Drizzle for D1
Section titled “Effect SQL and Drizzle for D1”Drizzle now works end to end on Cloudflare D1
(#887),
mirroring the Neon and PlanetScale integrations. At deploy time,
Drizzle.Schema diffs your schema module and writes pending
migration SQL, and passing schema.out as the database’s
migrationsDir makes the database apply it — generate first,
apply second, in one deploy:
export const Database = Effect.gen(function* () { const schema = yield* Drizzle.Schema("app-schema", { schema: "./src/schema.ts", out: "./migrations", dialect: "sqlite", });
return yield* Cloudflare.D1.Database("app-db", { migrationsDir: schema.out, });});At runtime, a Worker binds the database with
Cloudflare.D1.QueryDatabase and hands the client to
Drizzle.D1. Every query is an Effect with SqlError in the
typed error channel:
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const database = yield* Database; const d1 = yield* Cloudflare.D1.QueryDatabase(database); const db = yield* Drizzle.D1(d1, { relations });
return { fetch: Effect.gen(function* () { const users = yield* db.query.Users.findMany({ with: { posts: true }, }); return yield* HttpServerResponse.json({ users }); }), }; }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding)),) {}The low-level effect-sql clients also get one home: the new
alchemy/SQL namespace, with a raw tagged-template client for
each Drizzle integration. It binds inside a Worker the same way:
import * as SQL from "alchemy/SQL";
Effect.gen(function* () { const d1 = yield* Cloudflare.D1.QueryDatabase(database); const sql = yield* SQL.D1(d1);
return { fetch: Effect.gen(function* () { const rows = yield* sql`SELECT id, name FROM users`; return yield* HttpServerResponse.json({ rows }); }), };}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding));All clients share the same lifecycle: nothing connects at plan or deploy time, the client is built lazily on the first query, and it’s torn down when the request settles. The website’s new SQL hub collects effect-sql, Drizzle, and migrations in one place (#903).
Also in this release
Section titled “Also in this release”- Python Workers
(#861) —
point
mainat a.pyfile andCloudflare.Workerdeploys a Python Worker, with dependencies vendored frompyproject.tomlvia uv. Docs: Python Workers. - Workers AI binding
(#850) —
Cloudflare.Workers.AIruns models on Cloudflare’s GPU fleet from a Worker. - Durable Objects pin to a region
(#851) with
locationHint, and DO classes transfer between workers declaratively (#803). - GitHub Enterprise + deployment environments
(#845) —
alchemy loginaccepts an enterprise host (GHES and data residency), and the newGitHub.Environmentresource manages deployment environments. - Effect-native test runner
(#848) —
the whole alchemy test suite now runs in a single bun process
on
alchemy-test, with output designed for agents. Full story: A Test Runner Built for Agents. The live suite converged to green: 4,458 passed (#916). - RDS safety:
MasterUserPasswordis fingerprint-guarded so it’s no longer re-sent on every reconcile (#876), andskipFinalSnapshot: falsetakes a final snapshot on deliberate teardown (#877). AWS_REGIONoverrides the SSO profile’s stored region (#895).- Engine reliability — destroy aggregates GC failures and
refuses to silently orphan zombie rows
(#899,
#862),
--forcerefreshes stale bound attributes (#900), binding diffs are stable across runs (#898), unresolved Outputs are never persisted into state (#835), and truncated physical names stay unique (#868). - Actions get local bindings and Output accessors
(#807), and
alchemy loginworks without analchemy.run.ts(#842). - Migrating from v1 now adopts existing resources instead of destroying and recreating them (#819) — see Migrating from v1.
- Cloudflare fixes: queue-consumer wiring surviving mid-start
reconciles
(#917),
asset-upload retries
(#914),
wasm in
alchemy dev(#881), and Effect-valued env bindings converging in plans (#890). - The website now lives at alchemy.run (#806), builds 8x faster on Astro 7 (#873), and the Layers, Bindings, and Functions & Servers concept pages are rewritten.
Where to go next
Section titled “Where to go next”- AWS — the provider hub: setup, tutorial, and guides
- ECS and EKS
- Choosing a runtime
- SQL
- Bindings
- CHANGELOG
- Compare v2.0.0-beta.63 → v2.0.0-beta.64