| 1 | --- |
| 2 | title: 2.0.0-beta.75 - Railway, Fly, Hetzner & Websites |
| 3 | date: 2026-08-25T09:00:00Z |
| 4 | category: release |
| 5 | excerpt: 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 | |
| 8 | beta.75 ships **Railway, Fly.io, and Hetzner** — each with an |
| 9 | Effect-native `Service` platform. The |
| 10 | same website call now deploys to all three plus **Cloudflare and |
| 11 | AWS**: Vite, Astro, Next.js, Nuxt, SvelteKit, Waku, Octane, |
| 12 | Foldkit, and StaticSite, with one props vocabulary. |
| 13 | `alchemy dev` runs your AWS Lambda, S3, DynamoDB, SQS, ECS, |
| 14 | EC2, and website stack **on your machine** — no account, no |
| 15 | credentials, hot reload. SQL migrations have one format, |
| 16 | Workers can sit behind Access and receive inbound email, and |
| 17 | removal policies persist. |
| 18 | |
| 19 | This 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 | |
| 70 | effect 4.0 has moved from beta to release candidates, and alchemy |
| 71 | now 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 |
| 74 | minimum peer is `>=4.0.0-rc.112`; install effect and the platform |
| 75 | packages from the `rc` tag: |
| 76 | |
| 77 | ```sh |
| 78 | bun add alchemy@latest effect@rc @effect/platform-bun@rc @effect/platform-node@rc |
| 79 | ``` |
| 80 | |
| 81 | ## Websites on five clouds |
| 82 | |
| 83 | The Website family now deploys to **Cloudflare, AWS, Hetzner, |
| 84 | Railway, 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)). |
| 88 | Your `astro.config.*` loads natively. Stage-varying values go in |
| 89 | the `astro:` bag. Change the namespace to change the cloud: |
| 90 | |
| 91 | ```typescript |
| 92 | const 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 |
| 111 | props. Hetzner adds the DNS `zone` when you set `domain`: |
| 112 | |
| 113 | ```typescript |
| 114 | const 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 — |
| 124 | native HMR, no cloud resources. A Vite SPA is the same shape, |
| 125 | assets-only on AWS (no Lambda): |
| 126 | |
| 127 | ```typescript |
| 128 | const site = yield* AWS.Website.Vite("Web", { |
| 129 | domain: "app.example.com", |
| 130 | }); |
| 131 | ``` |
| 132 | |
| 133 | Next.js is the same call. Cloudflare and AWS run OpenNext; |
| 134 | Fly, Hetzner, and Railway run `next build` and a Node server: |
| 135 | |
| 136 | ```typescript |
| 137 | const site = yield* Fly.Website.Nextjs("Site", { |
| 138 | rootDir: "./apps/web", |
| 139 | env: { DATABASE_URL: db.connectionUri }, |
| 140 | domain: "app.example.com", |
| 141 | }); |
| 142 | ``` |
| 143 | |
| 144 | AWS'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 |
| 162 | Railway. React Router, SolidStart, and TanStack Start stay on |
| 163 | Cloudflare (`Website.Vite`) and AWS (dedicated composites). |
| 164 | Each cloud has a matching example |
| 165 | (`examples/{cloudflare,aws,fly,hetzner,railway}-website-{framework}`). |
| 166 | |
| 167 | Docs: |
| 168 | [Cloudflare](/cloudflare/frontend/frontends) · |
| 169 | [AWS](/aws/frontend/websites) · |
| 170 | [Fly](/fly) · |
| 171 | [Hetzner](/hetzner) · |
| 172 | [Railway](/railway). |
| 173 | |
| 174 | ## Railway |
| 175 | |
| 176 | A new **Railway provider** |
| 177 | ([#1295](https://github.com/alchemy-run/alchemy/pull/1295)): |
| 178 | Project, Service, Function, Postgres, MySQL, Mongo, Redis, |
| 179 | Bucket, Volume, Variable, CustomDomain, TcpProxy, and |
| 180 | PrivateNetwork. Auth is `RAILWAY_API_TOKEN` or `alchemy login`. |
| 181 | |
| 182 | A Project is the parent. `Railway.Service` is the |
| 183 | Function/Worker analog: an Effect program in a container. |
| 184 | Alchemy bundles `main`, builds a Docker image, and pushes it to |
| 185 | a registry Railway can pull (GHCR or Docker Hub — Railway has |
| 186 | no private registry of its own): |
| 187 | |
| 188 | ```typescript |
| 189 | const Site = Railway.Project("Site"); |
| 190 | |
| 191 | const Db = Railway.Postgres("Db", { project: Site }); |
| 192 | |
| 193 | export 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 |
| 215 | Buckets bind the same way: |
| 216 | |
| 217 | ```typescript |
| 218 | const Cache = Railway.Redis("Cache", { project: Site }); |
| 219 | const Data = Railway.Bucket("Data", { project: Site }); |
| 220 | |
| 221 | export 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 | |
| 238 | Pass `image` for a public tag (`hashicorp/http-echo`) or `repo` |
| 239 | for GitHub; `healthcheck` / `cronSchedule` match Railway IaC. |
| 240 | |
| 241 | `Railway.Function` is the canvas runtime: a single TypeScript |
| 242 | file on Bun. No Docker. No registry. Cap is 96KB. Distinct from |
| 243 | `Service({ main, registry })`: |
| 244 | |
| 245 | ```typescript |
| 246 | export 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 | |
| 255 | Docs: [Railway](/railway) · [Tutorial](/railway/tutorial/part-1). |
| 256 | |
| 257 | ## Hetzner |
| 258 | |
| 259 | A new **Hetzner Cloud provider** |
| 260 | ([#1258](https://github.com/alchemy-run/alchemy/pull/1258)): |
| 261 | Server, SshKey, Volume, VolumeAttachment, Network, Firewall, |
| 262 | PlacementGroup, PrimaryIp, FloatingIp, FloatingIpAssignment, |
| 263 | Certificate, LoadBalancer, Image, and DNS Zone + RecordSet. Auth |
| 264 | is a single `HCLOUD_TOKEN` (or `alchemy login`). |
| 265 | |
| 266 | ```typescript |
| 267 | export const Key = Hetzner.SshKey("laptop", { |
| 268 | publicKey: "ssh-ed25519 AAAA… you@laptop", |
| 269 | }); |
| 270 | |
| 271 | export 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 |
| 280 | own. Write an Effect program with a `fetch` handler; deploying |
| 281 | bundles it, ships it to the server, and runs it under systemd |
| 282 | with a readiness gate: |
| 283 | |
| 284 | ```typescript |
| 285 | export const Data = Hetzner.Volume("data", { |
| 286 | size: 40, |
| 287 | format: "ext4", |
| 288 | location: "nbg1", |
| 289 | server: Box, |
| 290 | }); |
| 291 | |
| 292 | export 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 | |
| 308 | Bindings follow the same capability model as AWS and Cloudflare: |
| 309 | `MountVolume` formats, attaches, and mounts a Volume into the |
| 310 | service, `Hetzner.Ssh(Box)` is a typed `exec`/`scp` client for a |
| 311 | Server, and the DNS zone splits into least-privilege |
| 312 | `ReadDns` / `WriteDns` / `ReadWriteDns` clients. Networking is a |
| 313 | private Network, Firewall rules, and a LoadBalancer with managed |
| 314 | TLS certificates: |
| 315 | |
| 316 | ```typescript |
| 317 | export 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 | |
| 333 | Hetzner gets its own docs tab |
| 334 | ([#1260](https://github.com/alchemy-run/alchemy/pull/1260)): a |
| 335 | setup guide, a four-part tutorial (server → service → volume → |
| 336 | network + load balancer), and guides for servers, services, |
| 337 | volumes, networking, and DNS. |
| 338 | |
| 339 | Docs: [Hetzner](/hetzner) · |
| 340 | [Tutorial](/hetzner/tutorial/part-1). |
| 341 | |
| 342 | ## Fly.io |
| 343 | |
| 344 | A new **Fly.io provider** |
| 345 | ([#1261](https://github.com/alchemy-run/alchemy/pull/1261)): App, |
| 346 | Machine, Service, Sprite, Volume + VolumeSnapshot, IpAssignment, |
| 347 | Certificate, Secret, SecretKey, and managed data — Postgres, |
| 348 | Upstash 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 |
| 352 | program in a Fly Machine. Alchemy bundles `main`, builds a Docker |
| 353 | image, pushes it, and Fly's Anycast proxy load-balances across |
| 354 | `count` replicas: |
| 355 | |
| 356 | ```typescript |
| 357 | const Site = Fly.App("Site"); |
| 358 | |
| 359 | const Db = Fly.Postgres("Db", { |
| 360 | region: "iad", |
| 361 | migrations: schema, // Drizzle.Schema, a directory, or { dir, table } |
| 362 | }); |
| 363 | |
| 364 | export 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 | |
| 380 | Upstash Redis and Tigris object storage bind the same way — |
| 381 | `ReadRedis` / `WriteRedis` / `ReadWriteRedis` on a `Fly.Redis`, |
| 382 | and S3-style `PutObject`/`GetObject` on a `Fly.Bucket`: |
| 383 | |
| 384 | ```typescript |
| 385 | const Cache = Fly.Redis("Cache"); // Upstash Redis |
| 386 | const Data = Fly.Bucket("Data"); // Tigris object storage |
| 387 | |
| 388 | export 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 | |
| 405 | The 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` |
| 409 | runs 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 |
| 413 | computer built for AI-agent workloads. It wakes on demand, |
| 414 | hibernates when idle, and keeps its filesystem between wakes. No |
| 415 | parent App, no Docker image: alchemy bundles `main` and writes it |
| 416 | straight onto the Sprite: |
| 417 | |
| 418 | ```typescript |
| 419 | export 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 | |
| 428 | Docs: [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 |
| 434 | account, no credentials: |
| 435 | |
| 436 | ```sh |
| 437 | bun alchemy dev |
| 438 | ``` |
| 439 | |
| 440 | Same program as production. A Function that writes to a bucket |
| 441 | and a queue: |
| 442 | |
| 443 | ```typescript |
| 444 | export 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 | |
| 467 | hits local endpoints: |
| 468 | |
| 469 | ```text |
| 470 | Api.functionUrl http://b71ff5c0….lambda-url.us-east-1.localhost:4566/ |
| 471 | Jobs.queueUrl http://localhost:4566/000000000000/jobs-dev-x7k2 |
| 472 | Files.bucketName files-dev-x7k2 |
| 473 | ``` |
| 474 | |
| 475 | Save the file and the Function serves the new code in |
| 476 | **~100–500ms**. `putObject` lands in the local bucket; queue |
| 477 | consumers fire; websites run their own dev servers. |
| 478 | |
| 479 | Opt a resource onto the real cloud with `Alchemy.remote()`: |
| 480 | |
| 481 | ```typescript |
| 482 | const secret = yield* AWS.SecretsManager.Secret("ProdSecret", { ... }) |
| 483 | .pipe(Alchemy.remote()); |
| 484 | ``` |
| 485 | |
| 486 | A remote resource is live end to end: its binding clients route |
| 487 | to the real cloud too, not just its lifecycle |
| 488 | ([#1406](https://github.com/alchemy-run/alchemy/pull/1406)). |
| 489 | Switching local ↔ live plans a **replacement**, so dev state |
| 490 | never silently becomes cloud state. |
| 491 | |
| 492 | Docs: [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 |
| 499 | program, two runtimes: |
| 500 | |
| 501 | ```typescript |
| 502 | export 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 |
| 514 | export 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 | |
| 525 | Save a file and both reload. EC2 instances that host an Effect |
| 526 | program do too. |
| 527 | |
| 528 | Docs: [AWS local development](/aws/local-development). |
| 529 | |
| 530 | ## One migration format for SQL databases |
| 531 | |
| 532 | Alchemy's migration bookkeeping was an invented table shape |
| 533 | incompatible with what drizzle-kit, Prisma, and wrangler actually |
| 534 | maintain. 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 |
| 537 | resource (D1, Neon, PlanetScale): |
| 538 | |
| 539 | ```typescript |
| 540 | const 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 |
| 546 | TypeScript schema against the checked-in snapshot and generates |
| 547 | SQL migrations on drift, and passing the resource itself orders |
| 548 | generation before application in the same deploy: |
| 549 | |
| 550 | ```typescript |
| 551 | const schema = yield* Drizzle.Schema("AppSchema", { |
| 552 | schema: "./src/schema.ts", |
| 553 | dialect: "sqlite", |
| 554 | }); |
| 555 | |
| 556 | const db = yield* Cloudflare.D1.Database("app-db", { |
| 557 | migrations: schema, |
| 558 | }); |
| 559 | ``` |
| 560 | |
| 561 | A database previously migrated with drizzle-kit, Prisma, or |
| 562 | wrangler is adopted on first deploy by a **one-way conversion**: |
| 563 | the old tool's applied history is copied into Alchemy's table |
| 564 | once, validated against your local migration files, and the old |
| 565 | table is left frozen — never written, never dropped. Nothing |
| 566 | replays. |
| 567 | |
| 568 | History validation is now strict — a recorded migration with no |
| 569 | matching local file fails the deploy instead of passing silently. |
| 570 | If you squashed and deleted old migration files, restore them |
| 571 | before upgrading. |
| 572 | |
| 573 | Docs: [SQL](/sql). |
| 574 | |
| 575 | ## Protect Workers with Access |
| 576 | |
| 577 | Support for |
| 578 | [Workers protected by Access](https://blog.cloudflare.com/workers-protected-by-access/) |
| 579 | ([#1228](https://github.com/alchemy-run/alchemy/pull/1228)): |
| 580 | declare policies inline on the Worker and alchemy manages the |
| 581 | dedicated Access application for you — |
| 582 | |
| 583 | ```typescript |
| 584 | export 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 |
| 595 | Workers (`access: App`), or protect **every current and future |
| 596 | Worker in the account** with the `AllWorkers` destination. |
| 597 | Enrollment goes through the binding contract, so removing the |
| 598 | prop (or the Worker) un-enrolls it on the next reconcile. |
| 599 | |
| 600 | At runtime, `Cloudflare.Access.Context` reads the authenticated |
| 601 | identity, and `dev: { access: { identity } }` stubs a logged-in |
| 602 | user under `alchemy dev` so you can test the gated paths locally: |
| 603 | |
| 604 | ```typescript |
| 605 | fetch: 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 | |
| 612 | Docs: |
| 613 | [Protect a Worker with Access](/cloudflare/security/access). |
| 614 | |
| 615 | ## Inbound email to Workers |
| 616 | |
| 617 | Workers 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 |
| 621 | you: |
| 622 | |
| 623 | ```typescript |
| 624 | export 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 | |
| 636 | Docs: [Email Workers](/cloudflare/email/email-worker). |
| 637 | |
| 638 | ## Removal policies persist |
| 639 | |
| 640 | `RemovalPolicy.retain()` added to an already-deployed resource |
| 641 | never reached state — the policy is a decoration, not a prop, so |
| 642 | the noop apply path skipped it, and a later rename or removal |
| 643 | orphan-deleted the resource anyway. beta.75 persists the policy |
| 644 | on the noop path |
| 645 | ([#1253](https://github.com/alchemy-run/alchemy/pull/1253)) and |
| 646 | prints a note whenever a deploy flips one. |
| 647 | |
| 648 | Two behavior changes come with it, both protective. Destroying a |
| 649 | non-empty R2 bucket now fails with `BucketNotEmpty` instead of |
| 650 | silently emptying it first — the old behavior deleted 60k objects |
| 651 | in the incident that motivated the fix. Opt disposable buckets |
| 652 | back in explicitly: |
| 653 | |
| 654 | ```typescript |
| 655 | const cache = yield* Cloudflare.R2.Bucket("Cache", { forceDestroy: true }); |
| 656 | ``` |
| 657 | |
| 658 | And because the policy now actually persists, a `retain()` you |
| 659 | *removed* from the code takes effect: the next removal deletes |
| 660 | the resource for real. If anything depends on a retain that's no |
| 661 | longer declared, put it back before deploying. |
| 662 | |
| 663 | Docs: |
| 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)). |
| 670 | Build from a Dockerfile into a managed ECR repo, or point |
| 671 | `image.uri` at an existing tag or digest. Zip XOR image at the |
| 672 | type level — mixing `main` with `image` is a compile error. |
| 673 | Thanks [Simon Westerlund](https://github.com/westerlund)! |
| 674 | |
| 675 | ```typescript |
| 676 | const 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) |