1---
2title: 2.0.0-beta.75 - Railway, Fly, Hetzner & Websites
3date: 2026-08-25T09:00:00Z
4category: release
5excerpt: v2.0.0-beta.75 ships Railway alongside Fly.io and Hetzner, deploys the same website frameworks to Cloudflare, AWS, Hetzner, Railway, and Fly, brings alchemy dev to AWS — Lambda, S3, DynamoDB, SQS, ECS, EC2, and websites on your machine with no credentials — and routes inbound email to Workers.
6---
7
8beta.75 ships **Railway, Fly.io, and Hetzner** — each with an
9Effect-native `Service` platform. The
10same website call now deploys to all three plus **Cloudflare and
11AWS**: Vite, Astro, Next.js, Nuxt, SvelteKit, Waku, Octane,
12Foldkit, and StaticSite, with one props vocabulary.
13`alchemy dev` runs your AWS Lambda, S3, DynamoDB, SQS, ECS,
14EC2, and website stack **on your machine** — no account, no
15credentials, hot reload. SQL migrations have one format,
16Workers can sit behind Access and receive inbound email, and
17removal policies persist.
18
19This post covers beta.72 through beta.75.
20
21:::caution
22**Breaking changes**
23
24- **effect `>=4.0.0-rc.112` is the new minimum peer**
25 ([#1245](https://github.com/alchemy-run/alchemy/pull/1245),
26 [#1318](https://github.com/alchemy-run/alchemy/pull/1318)) —
27 effect has moved from beta to release candidates.
28- **`@alchemy.run/cloudflare-frameworks` is now
29 `@alchemy.run/frontend-frameworks`**
30 ([#1140](https://github.com/alchemy-run/alchemy/pull/1140),
31 [#1154](https://github.com/alchemy-run/alchemy/pull/1154)) —
32 the framework integrations serve every cloud now. Update your
33 devDependency.
34- **Website props are one vocabulary**
35 ([#1353](https://github.com/alchemy-run/alchemy/pull/1353),
36 [#1351](https://github.com/alchemy-run/alchemy/pull/1351),
37 [#1158](https://github.com/alchemy-run/alchemy/pull/1158)) —
38 AWS's `server:` bag is gone, Cloudflare's mirrored framework
39 fields move into `astro:` / `waku:` /
40 `assets.notFoundHandling`, `StaticSite.environment` is
41 `build.env`. Lambda's `url` is `functionUrl`;
42 `Website.Server`'s `port` is `dev: { port }`. See
43 [below](#websites-on-five-clouds).
44- **SQL migrations have one prop and one format**
45 ([#1226](https://github.com/alchemy-run/alchemy/pull/1226)).
46 `migrationsDir`/`migrationsTable` are replaced by a single
47 `migrations` prop, and history lives in `__alchemy_migrations`.
48 Databases previously migrated with drizzle-kit, Prisma, or
49 wrangler are converted automatically on the next deploy — see
50 [below](#one-migration-format-for-sql-databases).
51- **`Worker.workerId` is now the immutable Worker ID**
52 ([#1228](https://github.com/alchemy-run/alchemy/pull/1228)) —
53 the hex script tag Access destinations key on, not a duplicate
54 of `workerName`. Old state heals itself on the next deploy.
55- **Destroying a non-empty R2 bucket now fails** instead of
56 silently deleting every object
57 ([#1253](https://github.com/alchemy-run/alchemy/pull/1253)) —
58 opt back in with `forceDestroy: true`. And a removed
59 `RemovalPolicy.retain()` now actually takes effect — see
60 [below](#removal-policies-persist).
61- **DynamoDB secondary indexes take typed keys**
62 ([#1216](https://github.com/alchemy-run/alchemy/pull/1216)) —
63 `partitionKey`/`sortKey` segments instead of the raw wire
64 `KeySchema` list. Existing state upgrades without a
65 replacement.
66:::
67
68## alchemy depends on effect@rc
69
70effect 4.0 has moved from beta to release candidates, and alchemy
71now depends on effect's `rc` dist-tag
72([#1245](https://github.com/alchemy-run/alchemy/pull/1245),
73[#1318](https://github.com/alchemy-run/alchemy/pull/1318)). The
74minimum peer is `>=4.0.0-rc.112`; install effect and the platform
75packages from the `rc` tag:
76
77```sh
78bun add alchemy@latest effect@rc @effect/platform-bun@rc @effect/platform-node@rc
79```
80
81## Websites on five clouds
82
83The Website family now deploys to **Cloudflare, AWS, Hetzner,
84Railway, and Fly**
85([#1140](https://github.com/alchemy-run/alchemy/pull/1140),
86[#1353](https://github.com/alchemy-run/alchemy/pull/1353),
87[#1351](https://github.com/alchemy-run/alchemy/pull/1351)).
88Your `astro.config.*` loads natively. Stage-varying values go in
89the `astro:` bag. Change the namespace to change the cloud:
90
91```typescript
92const site = yield* Cloudflare.Website.Astro("Blog", {
93 rootDir: "./apps/blog",
94 env: { API_BASE: api.url },
95 astro: { site: `https://${stage}.example.com`, output: "server" },
96 domain: "blog.example.com",
97});
98```
99
100```diff lang="typescript"
101-const site = yield* Cloudflare.Website.Astro("Blog", {
102+const site = yield* Fly.Website.Astro("Blog", {
103 rootDir: "./apps/blog",
104 env: { API_BASE: api.url },
105 astro: { site: `https://${stage}.example.com`, output: "server" },
106 domain: "blog.example.com",
107 });
108```
109
110`AWS.Website.Astro` and `Railway.Website.Astro` take the same
111props. Hetzner adds the DNS `zone` when you set `domain`:
112
113```typescript
114const site = yield* Hetzner.Website.Astro("Blog", {
115 rootDir: "./apps/blog",
116 env: { API_BASE: api.url },
117 astro: { site: `https://${stage}.example.com`, output: "server" },
118 domain: "blog.example.com",
119 zone,
120});
121```
122
123`alchemy dev` is the framework's own server on every cloud —
124native HMR, no cloud resources. A Vite SPA is the same shape,
125assets-only on AWS (no Lambda):
126
127```typescript
128const site = yield* AWS.Website.Vite("Web", {
129 domain: "app.example.com",
130});
131```
132
133Next.js is the same call. Cloudflare and AWS run OpenNext;
134Fly, Hetzner, and Railway run `next build` and a Node server:
135
136```typescript
137const site = yield* Fly.Website.Nextjs("Site", {
138 rootDir: "./apps/web",
139 env: { DATABASE_URL: db.connectionUri },
140 domain: "app.example.com",
141});
142```
143
144AWS's old `server:` bag is now flat props:
145
146```diff lang="typescript"
147 const site = yield* AWS.Website.Astro("Blog", {
148- server: {
149- memorySize: 1024,
150- environment: { API_URL: api.url },
151- },
152+ memorySize: 1024,
153+ env: { API_URL: api.url },
154 astro: { site: `https://${stage}.example.com` },
155 domain: "blog.example.com",
156 });
157```
158
159`Vite`, `Foldkit`, `Astro`, `Nextjs`, `Nuxt`, `SvelteKit`,
160`Waku`, `Octane`, and `StaticSite` on all five.
161`Cloudflare.Website.Vocs` also lands on Fly, Hetzner, and
162Railway. React Router, SolidStart, and TanStack Start stay on
163Cloudflare (`Website.Vite`) and AWS (dedicated composites).
164Each cloud has a matching example
165(`examples/{cloudflare,aws,fly,hetzner,railway}-website-{framework}`).
166
167Docs:
168[Cloudflare](/cloudflare/frontend/frontends) ·
169[AWS](/aws/frontend/websites) ·
170[Fly](/fly) ·
171[Hetzner](/hetzner) ·
172[Railway](/railway).
173
174## Railway
175
176A new **Railway provider**
177([#1295](https://github.com/alchemy-run/alchemy/pull/1295)):
178Project, Service, Function, Postgres, MySQL, Mongo, Redis,
179Bucket, Volume, Variable, CustomDomain, TcpProxy, and
180PrivateNetwork. Auth is `RAILWAY_API_TOKEN` or `alchemy login`.
181
182A Project is the parent. `Railway.Service` is the
183Function/Worker analog: an Effect program in a container.
184Alchemy bundles `main`, builds a Docker image, and pushes it to
185a registry Railway can pull (GHCR or Docker Hub — Railway has
186no private registry of its own):
187
188```typescript
189const Site = Railway.Project("Site");
190
191const Db = Railway.Postgres("Db", { project: Site });
192
193export default class Api extends Railway.Service<Api>()(
194 "Api",
195 {
196 project: Site,
197 main: import.meta.url,
198 registry: "ghcr.io/acme",
199 build: { install: ["pg"] },
200 },
201 Effect.gen(function* () {
202 const conn = yield* Railway.ConnectPostgres(Db);
203 const db = yield* Drizzle.Postgres(conn.connectionString);
204 return {
205 fetch: Effect.gen(function* () {
206 const rows = yield* db.select().from(users);
207 return HttpServerResponse.json({ rows });
208 }),
209 };
210 }).pipe(Effect.provide(Railway.ConnectPostgresHttp)),
211) {}
212```
213
214`api.url` is `https://{name}.up.railway.app`. Redis and S3-style
215Buckets bind the same way:
216
217```typescript
218const Cache = Railway.Redis("Cache", { project: Site });
219const Data = Railway.Bucket("Data", { project: Site });
220
221export default class Api extends Railway.Service<Api>()(
222 "Api",
223 { project: Site, main: import.meta.url, registry: "ghcr.io/acme" },
224 Effect.gen(function* () {
225 const cache = yield* Railway.ReadWriteRedis(Cache);
226 const putObject = yield* Railway.PutObject(Data);
227 return {
228 fetch: Effect.gen(function* () {
229 yield* putObject({ Key: "hello.txt", Body: "hello" });
230 yield* cache.set("last-write", "hello.txt");
231 return HttpServerResponse.text("ok");
232 }),
233 };
234 }).pipe(Effect.provide([Railway.ReadWriteRedisHttp, Railway.PutObjectHttp])),
235) {}
236```
237
238Pass `image` for a public tag (`hashicorp/http-echo`) or `repo`
239for GitHub; `healthcheck` / `cronSchedule` match Railway IaC.
240
241`Railway.Function` is the canvas runtime: a single TypeScript
242file on Bun. No Docker. No registry. Cap is 96KB. Distinct from
243`Service({ main, registry })`:
244
245```typescript
246export default class Ping extends Railway.Function<Ping>()(
247 "Ping",
248 { project: Site, main: import.meta.url },
249 Effect.gen(function* () {
250 return { fetch: Effect.succeed(HttpServerResponse.text("ok")) };
251 }),
252) {}
253```
254
255Docs: [Railway](/railway) · [Tutorial](/railway/tutorial/part-1).
256
257## Hetzner
258
259A new **Hetzner Cloud provider**
260([#1258](https://github.com/alchemy-run/alchemy/pull/1258)):
261Server, SshKey, Volume, VolumeAttachment, Network, Firewall,
262PlacementGroup, PrimaryIp, FloatingIp, FloatingIpAssignment,
263Certificate, LoadBalancer, Image, and DNS Zone + RecordSet. Auth
264is a single `HCLOUD_TOKEN` (or `alchemy login`).
265
266```typescript
267export const Key = Hetzner.SshKey("laptop", {
268 publicKey: "ssh-ed25519 AAAA… you@laptop",
269});
270
271export const Box = Hetzner.Server("box", {
272 serverType: "cpx12",
273 image: "ubuntu-24.04",
274 location: "nbg1",
275 sshKeys: [Key],
276});
277```
278
279`Hetzner.Service` is the Function/Worker analog for a server you
280own. Write an Effect program with a `fetch` handler; deploying
281bundles it, ships it to the server, and runs it under systemd
282with a readiness gate:
283
284```typescript
285export const Data = Hetzner.Volume("data", {
286 size: 40,
287 format: "ext4",
288 location: "nbg1",
289 server: Box,
290});
291
292export default class Api extends Hetzner.Service<Api>()(
293 "api",
294 { server: Box, main: import.meta.url, port: 3000 },
295 Effect.gen(function* () {
296 const disk = yield* Hetzner.MountVolume(Data, { path: "/var/lib/api" });
297 return {
298 fetch: Effect.gen(function* () {
299 const fs = yield* FileSystem.FileSystem;
300 const body = yield* fs.readFileString(`${disk.path}/hello.txt`);
301 return yield* HttpServerResponse.text(body);
302 }),
303 };
304 }).pipe(Effect.provide(Hetzner.MountVolumeLive)),
305) {}
306```
307
308Bindings follow the same capability model as AWS and Cloudflare:
309`MountVolume` formats, attaches, and mounts a Volume into the
310service, `Hetzner.Ssh(Box)` is a typed `exec`/`scp` client for a
311Server, and the DNS zone splits into least-privilege
312`ReadDns` / `WriteDns` / `ReadWriteDns` clients. Networking is a
313private Network, Firewall rules, and a LoadBalancer with managed
314TLS certificates:
315
316```typescript
317export const Lb = Hetzner.LoadBalancer("lb", {
318 location: "nbg1",
319 loadBalancerType: "lb11",
320 targets: [{ type: "server", server: Box, usePrivateIp: true }],
321 services: [
322 {
323 protocol: "https",
324 listenPort: 443,
325 destinationPort: 3000,
326 certificates: [Managed],
327 http: { redirectHttp: true },
328 },
329 ],
330});
331```
332
333Hetzner gets its own docs tab
334([#1260](https://github.com/alchemy-run/alchemy/pull/1260)): a
335setup guide, a four-part tutorial (server → service → volume →
336network + load balancer), and guides for servers, services,
337volumes, networking, and DNS.
338
339Docs: [Hetzner](/hetzner) ·
340[Tutorial](/hetzner/tutorial/part-1).
341
342## Fly.io
343
344A new **Fly.io provider**
345([#1261](https://github.com/alchemy-run/alchemy/pull/1261)): App,
346Machine, Service, Sprite, Volume + VolumeSnapshot, IpAssignment,
347Certificate, Secret, SecretKey, and managed data — Postgres,
348Upstash Redis, and Tigris object storage. Auth is a single
349`FLY_API_TOKEN`.
350
351`Fly.Service` is the Function/Worker analog: an always-on Effect
352program in a Fly Machine. Alchemy bundles `main`, builds a Docker
353image, pushes it, and Fly's Anycast proxy load-balances across
354`count` replicas:
355
356```typescript
357const Site = Fly.App("Site");
358
359const Db = Fly.Postgres("Db", {
360 region: "iad",
361 migrations: schema, // Drizzle.Schema, a directory, or { dir, table }
362});
363
364export default class Api extends Fly.Service<Api>()(
365 "Api",
366 { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 },
367 Effect.gen(function* () {
368 const conn = yield* Fly.ConnectPostgres(Db);
369 const db = yield* Drizzle.Postgres(conn.connectionString);
370 return {
371 fetch: Effect.gen(function* () {
372 const rows = yield* db.select().from(users);
373 return HttpServerResponse.json({ rows });
374 }),
375 };
376 }).pipe(Effect.provide(Fly.ConnectPostgresHttp)),
377) {}
378```
379
380Upstash Redis and Tigris object storage bind the same way —
381`ReadRedis` / `WriteRedis` / `ReadWriteRedis` on a `Fly.Redis`,
382and S3-style `PutObject`/`GetObject` on a `Fly.Bucket`:
383
384```typescript
385const Cache = Fly.Redis("Cache"); // Upstash Redis
386const Data = Fly.Bucket("Data"); // Tigris object storage
387
388export default class Api extends Fly.Service<Api>()(
389 "Api",
390 { app: Site, main: import.meta.url, port: 3000 },
391 Effect.gen(function* () {
392 const cache = yield* Fly.ReadWriteRedis(Cache);
393 const putObject = yield* Fly.PutObject(Data);
394 return {
395 fetch: Effect.gen(function* () {
396 yield* putObject({ Key: "hello.txt", Body: "hello" });
397 yield* cache.set("last-write", "hello.txt");
398 return HttpServerResponse.text("ok");
399 }),
400 };
401 }).pipe(Effect.provide([Fly.ReadWriteRedisHttp, Fly.PutObjectHttp])),
402) {}
403```
404
405The rest of the surface follows the same capability model:
406`GetSecret`/`WriteSecret` on app secrets,
407`Encrypt`/`Decrypt`/`Sign`/`Verify` on a `SecretKey`, and
408`MountVolume` for disks. Already have an image? `Fly.Machine`
409runs it as one VM per resource — yield another to add capacity.
410
411`Fly.Sprite` deploys an Effect program onto a
412[Fly Sprite](https://fly.io/sprites) — a persistent Linux
413computer built for AI-agent workloads. It wakes on demand,
414hibernates when idle, and keeps its filesystem between wakes. No
415parent App, no Docker image: alchemy bundles `main` and writes it
416straight onto the Sprite:
417
418```typescript
419export default class Box extends Fly.Sprite<Box>()(
420 "Box",
421 { main: import.meta.url },
422 Effect.gen(function* () {
423 return { fetch: Effect.succeed(HttpServerResponse.text("hello")) };
424 }),
425) {}
426```
427
428Docs: [Fly.io](/fly) · [Tutorial](/fly/tutorial/part-1).
429
430## AWS local dev
431
432`alchemy dev` now runs your AWS stack on your machine
433([#1154](https://github.com/alchemy-run/alchemy/pull/1154)). No
434account, no credentials:
435
436```sh
437bun alchemy dev
438```
439
440Same program as production. A Function that writes to a bucket
441and a queue:
442
443```typescript
444export default class Api extends AWS.Lambda.Function<Api>()(
445 "Api",
446 { main: import.meta.url, functionUrl: true },
447 Effect.gen(function* () {
448 const files = yield* AWS.S3.Bucket("Files");
449 const jobs = yield* AWS.SQS.Queue("Jobs");
450
451 const putObject = yield* AWS.S3.PutObject(files);
452 const sendMessage = yield* AWS.SQS.SendMessage(jobs);
453
454 return {
455 fetch: Effect.gen(function* () {
456 yield* putObject({ Key: "hello.txt", Body: "hello" });
457 yield* sendMessage({ MessageBody: "process hello.txt" });
458 return yield* HttpServerResponse.text("ok");
459 }).pipe(Effect.orDie),
460 };
461 }).pipe(
462 Effect.provide([AWS.S3.PutObjectHttp, AWS.SQS.SendMessageHttp]),
463 ),
464) {}
465```
466
467hits local endpoints:
468
469```text
470Api.functionUrl http://b71ff5c0….lambda-url.us-east-1.localhost:4566/
471Jobs.queueUrl http://localhost:4566/000000000000/jobs-dev-x7k2
472Files.bucketName files-dev-x7k2
473```
474
475Save the file and the Function serves the new code in
476**~100–500ms**. `putObject` lands in the local bucket; queue
477consumers fire; websites run their own dev servers.
478
479Opt a resource onto the real cloud with `Alchemy.remote()`:
480
481```typescript
482const secret = yield* AWS.SecretsManager.Secret("ProdSecret", { ... })
483 .pipe(Alchemy.remote());
484```
485
486A remote resource is live end to end: its binding clients route
487to the real cloud too, not just its lifecycle
488([#1406](https://github.com/alchemy-run/alchemy/pull/1406)).
489Switching local ↔ live plans a **replacement**, so dev state
490never silently becomes cloud state.
491
492Docs: [AWS local development](/aws/local-development).
493
494## Local ECS and MicroVMs
495
496`alchemy dev` runs ECS tasks and Lambda MicroVMs the same way
497([#1185](https://github.com/alchemy-run/alchemy/pull/1185),
498[#1335](https://github.com/alchemy-run/alchemy/pull/1335)) — same
499program, two runtimes:
500
501```typescript
502export default class Web extends AWS.ECS.Task<Web>()(
503 "Web",
504 { main: import.meta.url, port: 8080 },
505 Effect.gen(function* () {
506 return {
507 fetch: Effect.succeed(HttpServerResponse.text("hello")),
508 };
509 }),
510) {}
511```
512
513```typescript
514export default class Box extends AWS.Lambda.MicrovmImage<Box>()(
515 "Box",
516 { main: import.meta.url },
517 Effect.gen(function* () {
518 return {
519 fetch: Effect.succeed(HttpServerResponse.text("hello")),
520 };
521 }),
522) {}
523```
524
525Save a file and both reload. EC2 instances that host an Effect
526program do too.
527
528Docs: [AWS local development](/aws/local-development).
529
530## One migration format for SQL databases
531
532Alchemy's migration bookkeeping was an invented table shape
533incompatible with what drizzle-kit, Prisma, and wrangler actually
534maintain. There is now exactly **one** format, owned by Alchemy
535([#1226](https://github.com/alchemy-run/alchemy/pull/1226)):
536`__alchemy_migrations`, with one prop on every SQL database
537resource (D1, Neon, PlanetScale):
538
539```typescript
540const db = yield* Cloudflare.D1.Database("app-db", {
541 migrations: "./migrations", // or { dir, table }
542});
543```
544
545`Drizzle.Schema` plugs into the same prop. It diffs your
546TypeScript schema against the checked-in snapshot and generates
547SQL migrations on drift, and passing the resource itself orders
548generation before application in the same deploy:
549
550```typescript
551const schema = yield* Drizzle.Schema("AppSchema", {
552 schema: "./src/schema.ts",
553 dialect: "sqlite",
554});
555
556const db = yield* Cloudflare.D1.Database("app-db", {
557 migrations: schema,
558});
559```
560
561A database previously migrated with drizzle-kit, Prisma, or
562wrangler is adopted on first deploy by a **one-way conversion**:
563the old tool's applied history is copied into Alchemy's table
564once, validated against your local migration files, and the old
565table is left frozen — never written, never dropped. Nothing
566replays.
567
568History validation is now strict — a recorded migration with no
569matching local file fails the deploy instead of passing silently.
570If you squashed and deleted old migration files, restore them
571before upgrading.
572
573Docs: [SQL](/sql).
574
575## Protect Workers with Access
576
577Support for
578[Workers protected by Access](https://blog.cloudflare.com/workers-protected-by-access/)
579([#1228](https://github.com/alchemy-run/alchemy/pull/1228)):
580declare policies inline on the Worker and alchemy manages the
581dedicated Access application for you —
582
583```typescript
584export default class Api extends Cloudflare.Worker<Api>()("Api", {
585 main: import.meta.url,
586 access: {
587 policies: [
588 { decision: "allow", include: [{ emailDomain: "example.com" }] },
589 ],
590 },
591}, /* ... */) {}
592```
593
594— or share one `Cloudflare.Access.Application` across several
595Workers (`access: App`), or protect **every current and future
596Worker in the account** with the `AllWorkers` destination.
597Enrollment goes through the binding contract, so removing the
598prop (or the Worker) un-enrolls it on the next reconcile.
599
600At runtime, `Cloudflare.Access.Context` reads the authenticated
601identity, and `dev: { access: { identity } }` stubs a logged-in
602user under `alchemy dev` so you can test the gated paths locally:
603
604```typescript
605fetch: Effect.gen(function* () {
606 const access = yield* Cloudflare.Access.Context;
607 const identity = yield* access.getIdentity(); // email, groups, idp…
608 return yield* HttpServerResponse.json({ email: identity?.email });
609}),
610```
611
612Docs:
613[Protect a Worker with Access](/cloudflare/security/access).
614
615## Inbound email to Workers
616
617Workers can now receive email
618([#401](https://github.com/alchemy-run/alchemy/pull/401)).
619`Cloudflare.email({ zone }).subscribe(handler)` registers the
620`email` handler and provisions the zone's email routing rules for
621you:
622
623```typescript
624export default Cloudflare.Worker(
625 "Inbox",
626 { main: import.meta.url },
627 Effect.gen(function* () {
628 yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
629 message.forward("ops@example.com"),
630 );
631 return {};
632 }).pipe(Effect.provide(Cloudflare.EmailEventSourceLive)),
633);
634```
635
636Docs: [Email Workers](/cloudflare/email/email-worker).
637
638## Removal policies persist
639
640`RemovalPolicy.retain()` added to an already-deployed resource
641never reached state — the policy is a decoration, not a prop, so
642the noop apply path skipped it, and a later rename or removal
643orphan-deleted the resource anyway. beta.75 persists the policy
644on the noop path
645([#1253](https://github.com/alchemy-run/alchemy/pull/1253)) and
646prints a note whenever a deploy flips one.
647
648Two behavior changes come with it, both protective. Destroying a
649non-empty R2 bucket now fails with `BucketNotEmpty` instead of
650silently emptying it first — the old behavior deleted 60k objects
651in the incident that motivated the fix. Opt disposable buckets
652back in explicitly:
653
654```typescript
655const cache = yield* Cloudflare.R2.Bucket("Cache", { forceDestroy: true });
656```
657
658And because the policy now actually persists, a `retain()` you
659*removed* from the code takes effect: the next removal deletes
660the resource for real. If anything depends on a retain that's no
661longer declared, put it back before deploying.
662
663Docs:
664[Resource lifecycle](/infrastructure-as-code/resource-lifecycle).
665
666## Lambda Docker Functions
667
668`AWS.Lambda.Function` deploys a container image
669([#1077](https://github.com/alchemy-run/alchemy/pull/1077)).
670Build from a Dockerfile into a managed ECR repo, or point
671`image.uri` at an existing tag or digest. Zip XOR image at the
672type level — mixing `main` with `image` is a compile error.
673Thanks [Simon Westerlund](https://github.com/westerlund)!
674
675```typescript
676const api = yield* AWS.Lambda.Function("ContainerApi", {
677 image: { context: "./lambda", dockerfile: "Dockerfile" },
678 architecture: "arm64",
679 functionUrl: true,
680});
681```
682
683## Also in this release
684
685- **`Cloudflare.Website.Vocs`**
686 ([#1278](https://github.com/alchemy-run/alchemy/pull/1278)) —
687 first-class Vocs docs sites, also on Fly, Hetzner, and Railway.
688- **Isolated installs bundle**
689 ([#1324](https://github.com/alchemy-run/alchemy/pull/1324)) —
690 generated entries are shims over real
691 `alchemy/Runtime/Bootstrap/*` modules, so bun
692 `--linker=isolated` and pnpm no longer leave Effect programs
693 crashing at boot with `Cannot find module`.
694- **Axiom dataset edge deployments**
695 ([#1361](https://github.com/alchemy-run/alchemy/pull/1361)) —
696 `edgeDeployment` on `Axiom.Dataset`; OTLP endpoints derive
697 from Axiom's regional URL. Thanks
698 [Aman Varshney](https://github.com/AmanVarshney01)!
699- **Cognito email OTP, CustomEmailSender, and KMS**
700 ([#1323](https://github.com/alchemy-run/alchemy/pull/1323)) —
701 on `UserPool`.
702- **PlanetScale Metal SKUs**
703 ([#1319](https://github.com/alchemy-run/alchemy/pull/1319)) —
704 on `PostgresClusterSize`.
705- **`r2.dev` managed domain**
706 ([#1329](https://github.com/alchemy-run/alchemy/pull/1329)) —
707 enable it on `Cloudflare.R2.Bucket`.
708- **`nuke --local` clears the emulator**
709 ([#1287](https://github.com/alchemy-run/alchemy/pull/1287)) —
710 `alchemy unsafe nuke --local` enumerates and deletes through
711 each provider's **local** implementation (the floci-emulated
712 account, the Cloudflare local runtime), so wiping local state
713 is one command with no cloud credentials in reach.
714- **Hetzner init scripts compose with the bootstrap**
715 ([#1289](https://github.com/alchemy-run/alchemy/pull/1289)) —
716 `Hetzner.Server.userData` merges into a multipart cloud-init
717 document alongside alchemy's bootstrap instead of replacing it,
718 and changing it plans a replacement (cloud-init only runs on
719 first boot).
720- **Prisma: object storage, locked Postgres state, and deploy
721 triggers**
722 ([#1061](https://github.com/alchemy-run/alchemy/pull/1061)) —
723 `Prisma.Bucket` + `BucketAccessKey` resources with
724 `ReadBucket`/`WriteBucket`/`ReadWriteBucket` bindings, a
725 `State/PostgresState` backend with per-stack advisory locks,
726 and `Deployment.triggers` to force a redeploy when an opaque
727 key/value (e.g. a rotated secret) changes. Thanks
728 [Will Madden](https://github.com/wmadden)!
729- **Structured DNS record data**
730 ([#1257](https://github.com/alchemy-run/alchemy/pull/1257)) —
731 Cloudflare DNS records accept typed component objects for SVCB,
732 HTTPS, SRV, CAA, and friends via the same `content` prop, with
733 the shape discriminated by `type`. Thanks
734 [Arya Saatvik](https://github.com/aryasaatvik)! Adoption also
735 disambiguates records sharing `(name, type)` by
736 content/priority instead of clobbering the first match
737 ([#1264](https://github.com/alchemy-run/alchemy/pull/1264)).
738- **Worker versions carry static assets**
739 ([#1266](https://github.com/alchemy-run/alchemy/pull/1266)) — a
740 preview version can ship its own assets without touching the
741 parent deployment. Thanks
742 [Rahul Mishra](https://github.com/BlankParticle), who also
743 migrated the workspace to pnpm
744 ([#1214](https://github.com/alchemy-run/alchemy/pull/1214)),
745 bumped effect to rc.110 then rc.111
746 ([#1245](https://github.com/alchemy-run/alchemy/pull/1245),
747 [#1318](https://github.com/alchemy-run/alchemy/pull/1318)),
748 made process cleanup explicit and scoped
749 ([#1175](https://github.com/alchemy-run/alchemy/pull/1175)),
750 and improved platform runtime detection
751 ([#1197](https://github.com/alchemy-run/alchemy/pull/1197))!
752- **Cloudflare Containers reach host services in dev**
753 ([#1344](https://github.com/alchemy-run/alchemy/pull/1344),
754 [#1335](https://github.com/alchemy-run/alchemy/pull/1335),
755 [#1366](https://github.com/alchemy-run/alchemy/pull/1366)) —
756 loopback env URLs rewrite to the docker host, container images
757 hot-reload on context/Dockerfile/prop changes, and Linux dev
758 containers reach loopback host databases through a unix-socket
759 tunnel instead of the (often firewalled) bridge IP. A failed
760 `docker build` now fails the dev deploy with docker's real
761 error — and a buildx hint when `--load` is the problem —
762 instead of looping on "Container exited while waiting for port"
763 ([#1377](https://github.com/alchemy-run/alchemy/pull/1377)).
764- **`Docker.Container` fixes**
765 ([#1398](https://github.com/alchemy-run/alchemy/pull/1398)) —
766 `ports: [{ external: 0 }]` reports the actually-assigned host
767 port, `extraHosts` forwards as `--add-host`, and reconciliation
768 only disconnects networks alchemy itself attached instead of
769 tearing off Docker's default `bridge`.
770- **Nested Worker env plans converge**
771 ([#1273](https://github.com/alchemy-run/alchemy/pull/1273)) — a
772 Worker bound to an already-yielded Worker no longer plans
773 `update` forever on identical deploys. Thanks
774 [Odysseas Papadimas](https://github.com/odysseaspapadimas)!
775- **`alchemy dev` survives a broken save**
776 ([#1259](https://github.com/alchemy-run/alchemy/pull/1259)) — a
777 mid-edit save whose module evaluation throws now logs and waits
778 for the next change instead of killing the watch session.
779- **Env-bound strings deploy bare**
780 ([#1254](https://github.com/alchemy-run/alchemy/pull/1254)) — a
781 queue name no longer lands in its binding JSON-quoted, so the
782 dashboard and hand-written env reads see the real value.
783- **App Runner reaps auto-created log groups on delete**
784 ([#1327](https://github.com/alchemy-run/alchemy/pull/1327)).
785- **SvelteKit 3 config shape**
786 ([#1347](https://github.com/alchemy-run/alchemy/pull/1347)) —
787 `@sveltejs/kit@3.0.0-next.21+`.
788- **Process env wins over dotenv defaults**
789 ([#1355](https://github.com/alchemy-run/alchemy/pull/1355)).
790- **Dependency security bumps**
791 ([#1255](https://github.com/alchemy-run/alchemy/pull/1255)) —
792 the vulnerable `extract-zip` and libvips advisories are out of
793 the runtime's dependency closure.
794- **`Worker.domain` pins its zone**
795 ([#1241](https://github.com/alchemy-run/alchemy/pull/1241)) —
796 `zoneId`/`zone`/`zoneName` on `domain`, and unpinned hostnames
797 resolve by name instead of the first page of an account-wide
798 zone list. Thanks [Ben Snyder](https://github.com/benpsnyder)!
799- **`Cloudflare.Website.Foldkit`**
800 ([#1120](https://github.com/alchemy-run/alchemy/pull/1120)) and
801 **per-workflow step limits**
802 (`limits: { steps }`,
803 [#816](https://github.com/alchemy-run/alchemy/pull/816)) —
804 thanks [Filip Falcon](https://github.com/filipfalcon)!
805- **Pipelines streams bind to Workers**
806 ([#1176](https://github.com/alchemy-run/alchemy/pull/1176)) — a
807 stream in `env` becomes a real pipelines binding instead of
808 falling through to JSON. Thanks
809 [spinx](https://github.com/spinx)!
810- **DynamoDB multi-attribute GSI keys**
811 ([#1216](https://github.com/alchemy-run/alchemy/pull/1216)) —
812 GSI partition and sort keys composed of up to 4 attributes
813 each, with order-preserving diffs.
814- **RDS `DBParameterGroup.parameters`**
815 ([#1205](https://github.com/alchemy-run/alchemy/pull/1205)) —
816 engine settings reconciled against live user-modified values.
817 Thanks [skusez](https://github.com/skusez)!
818- **Lambda packaging fixes** — installed packages keep their file
819 modes and symlinks
820 ([#855](https://github.com/alchemy-run/alchemy/pull/855),
821 thanks [Joaquín Pérez](https://github.com/jperezrealini)!) and
822 nested `zipCode` archives are byte-deterministic across builds
823 ([#1211](https://github.com/alchemy-run/alchemy/pull/1211)).
824- **Astro forwards `prerenderEnvironment`**
825 ([#1242](https://github.com/alchemy-run/alchemy/pull/1242)) —
826 thanks [Maxwell Brown](https://github.com/IMax153)!
827- **The CLI honors `STAGE`**
828 ([#1244](https://github.com/alchemy-run/alchemy/pull/1244)) —
829 thanks [sethcarlton](https://github.com/sethcarlton)! And the
830 version check never suggests a downgrade
831 ([#1116](https://github.com/alchemy-run/alchemy/pull/1116)).
832- **`@effect/vitest` moves to devDependencies**
833 ([#1191](https://github.com/alchemy-run/alchemy/pull/1191)) —
834 installing alchemy no longer pulls a test toolchain (and its
835 esbuild peer conflicts) into your resolution. Thanks
836 [Kristóf Siket](https://github.com/kristof-siket)!
837- **A missing `.make` Layer names itself**
838 ([#1055](https://github.com/alchemy-run/alchemy/pull/1055)) —
839 yielding a bare-tag resource without its implementation Layer
840 dies with a `MissingImplementationError` explaining exactly
841 what to provide, instead of a `TypeError` deep in a provider.
842 Thanks [apostoli](https://github.com/apostolos-geyer)!
843- **Circular Worker layers typecheck**
844 ([#1199](https://github.com/alchemy-run/alchemy/pull/1199)) —
845 `Worker.make` no longer leaks the other Worker's tag into
846 `Layer.mergeAll` requirements. Thanks
847 [Patrik Duksin](https://github.com/patrikduksin)!
848
849## Where to go next
850
851- [Railway](/railway)
852- [Railway tutorial](/railway/tutorial/part-1)
853- [Fly.io](/fly)
854- [Fly.io tutorial](/fly/tutorial/part-1)
855- [Hetzner](/hetzner)
856- [Hetzner tutorial](/hetzner/tutorial/part-1)
857- [Cloudflare frontends](/cloudflare/frontend/frontends)
858- [AWS frontends](/aws/frontend/websites)
859- [AWS local development](/aws/local-development)
860- [Local development](/environments/local-development)
861- [Protect a Worker with Access](/cloudflare/security/access)
862- [SQL](/sql)
863- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md)
864- [Compare v2.0.0-beta.71 → main](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.71...main)