1---
2title: 2.0.0-beta.80 - GCP, Neon & Secret Providers
3date: 2026-09-29T09:00:00Z
4category: release
5excerpt: v2.0.0-beta.80 adds a Google Cloud provider — ~1,050 resources across ~190 services, Effect-native Cloud Run and Cloud Functions runtimes, least-privilege IAM bindings, and event sources — turns Neon into a full backend provider with Functions, storage, Auth, and 13 website frameworks, and lets a stack load its secrets from dotenv, Doppler, or Infisical, with OIDC in CI. Also Effect 4.0 stable, Effect-native Prisma ORM v8, Fly Services with typed service-to-service calls and blue/green deploys, --include/--exclude deploys, durable callbacks, hibernating WebSocket RPC, and presigned R2 URLs in alchemy dev.
6---
7
8beta.80 has three headline additions. **Google Cloud** arrives with
9about 1,050 resources across 190 services, Effect-native runtimes on
10Cloud Run, Cloud Functions, and GKE, and bindings that grant each host
11the narrowest IAM role GCP allows. **Neon** becomes a full backend
12provider: Functions, object storage, managed Auth, the Data API, AI
13Gateway, and 13 `Neon.Website` frameworks, all in one stack. **Secret
14providers** let a stack load its `Config` from dotenv files,
15**Doppler**, or **Infisical**, per stage, with browser login on your
16machine and OIDC in CI.
17
18**Effect 4.0 stable** is now the baseline. **Prisma ORM v8** also lands with an Effect-native Postgres runtime. A
19**`Fly.Service`** is now its own App with a public URL, typed calls to
20other Services, blue/green deploys, and multi-container Machines, and
21**`--include` / `--exclude`** deploy a slice of a stack. On Cloudflare:
22durable callbacks, hibernating WebSocket RPC, presigned R2 URLs under
23`alchemy dev`, and Queue subscriptions from any resource.
24
25:::caution
26**Breaking changes**
27
28- **Effect 4.0 stable is the new baseline**
29 ([#1894](https://github.com/alchemy-run/alchemy/pull/1894),
30 [#1899](https://github.com/alchemy-run/alchemy/pull/1899)). Upgrade
31 `effect` and the Effect platform packages together:
32
33 ```sh
34 bun add alchemy@latest effect@^4.0.0 @effect/platform-bun@^4.0.0 @effect/platform-node@^4.0.0
35 ```
36
37- **Better Auth 1.7.5+**
38 ([#1732](https://github.com/alchemy-run/alchemy/pull/1732)). Drizzle
39 users install `@better-auth/drizzle-adapter`, adopt Relations v2, and
40 regenerate the auth schema. Custom secondary storage must implement
41 `increment(key, ttlSeconds)` with a required TTL, and `increment` /
42 `getAndDelete` must be atomic. See
43 [Upgrading from 1.6 to 1.7](/better-auth/upgrades/from-1-6-to-1-7).
44- **`Cloudflare.AI.Model` is now the Workers AI catalog resource**
45 ([#1722](https://github.com/alchemy-run/alchemy/pull/1722)). The AI
46 Search model-name type that used this name is now `AI.SearchModel`.
47- **JavaScript Workers default to `new_module_registry`**
48 ([#1663](https://github.com/alchemy-run/alchemy/pull/1663)) in
49 deploys, `alchemy dev`, and Vite preview. Opt out with
50 `compatibility: { flags: ["legacy_module_registry"] }`. Python Workers
51 are unchanged.
52- **`Fly.Machine` has no `url`**
53 ([#1794](https://github.com/alchemy-run/alchemy/pull/1794)). Use a
54 `Fly.Service` when you need an endpoint.
55- **Durable Object transactions are stricter**
56 ([#1668](https://github.com/alchemy-run/alchemy/pull/1668)). Storage
57 access from a sibling fiber inside `storage.transaction`, and writes
58 after `txn.rollback()`, now fail.
59- **`@distilled.cloud/hetzner` exports services from the package root**
60 ([#1784](https://github.com/alchemy-run/alchemy/pull/1784)). Only
61 affects code that calls the SDK directly:
62
63 ```diff lang="typescript"
64 -import { Services } from "@distilled.cloud/hetzner";
65 -Services.servers.getServer({ id });
66 +import * as Hetzner from "@distilled.cloud/hetzner";
67 +Hetzner.servers.getServer({ id });
68 ```
69
70 The latest distilled also renames Fly's `updateMachineMetadata` to
71 `patchMachineMetadata` and PlanetScale's `createKeyspaceResizeRequest`
72 to `updateKeyspaceResizeRequest`, and the Railway SDK moves to
73 `Query.fn` lenses
74 ([#1891](https://github.com/alchemy-run/alchemy/pull/1891)).
75:::
76
77## Google Cloud
78
79`alchemy/GCP` is a new provider
80([#1336](https://github.com/alchemy-run/alchemy/pull/1336)) — thanks
81[Michael K](https://github.com/Mkassabov)! It covers about 1,050
82resources across 190 services, with Effect-native runtimes on every GCP
83compute host:
84
85| Host | Effect runtime |
86| --- | --- |
87| `GCP.Function` (a Cloud Run Service) | `fetch` |
88| `GCP.Run.Job` | `run`, exits on completion |
89| `GCP.Run.WorkerPool` | `run`, long-lived |
90| `GCP.CloudFunctions.Function` | `fetch` on Node 22 |
91| `Kubernetes.Deployment` / `Job` on GKE | same as EKS, through Workload Identity |
92
93Bindings follow the same shape as AWS and Cloudflare. Each `yield*`
94grants one role to the host's runtime service account, scoped as tightly
95as GCP allows, and the grant is revoked when the binding is removed.
96
97Here is an order service: a Cloud Run API that stores orders in
98Firestore and publishes an event, and a second service that consumes
99those events from Pub/Sub. Start with the database and the queue:
100
101```typescript
102// src/resources.ts
103import * as GCP from "alchemy/GCP";
104
105export const Orders = GCP.Firestore.Database("Orders", {
106 location: "us-central1",
107 type: "FIRESTORE_NATIVE",
108});
109
110export const OrderEvents = GCP.PubSub.Topic("OrderEvents", {});
111```
112
113### Websites on Cloud Run
114
115`GCP.Website` adds framework websites and `StaticSite` on Cloud Run
116([#1898](https://github.com/alchemy-run/alchemy/pull/1898)). Choose from
117Next.js, Nuxt, Astro, SvelteKit, TanStack Start, React Router, SolidStart,
118Waku, Vite, Vocs, Vinext, Foldkit, and Octane. The shared options cover
119public access, ingress, scaling, and container resources.
120
121```typescript
122const site = yield* GCP.Website.Nextjs("Web", {
123 rootDir: "./web",
124});
125```
126
127### A database
128
129The API writes each order to Firestore and announces it on the topic:
130
131```typescript
132// src/Api.ts
133import * as GCP from "alchemy/GCP";
134import * as Effect from "effect/Effect";
135import { HttpServerRequest } from "effect/http/HttpServerRequest";
136import * as HttpServerResponse from "effect/http/HttpServerResponse";
137import { OrderEvents, Orders } from "./resources.ts";
138
139export default class Api extends GCP.Function<Api>()(
140 "Api",
141 { main: import.meta.url, location: "us-central1", invokerIamDisabled: true },
142 Effect.gen(function* () {
143 // datastore.user, under an IAM Condition naming only this database
144 const db = yield* GCP.Firestore.ReadWriteDatabase(Orders);
145 // pubsub.publisher on this topic only
146 const events = yield* GCP.PubSub.WriteTopic(OrderEvents);
147
148 return {
149 fetch: Effect.gen(function* () {
150 const request = yield* HttpServerRequest;
151 const { email, total } = (yield* request.json) as { email: string; total: number };
152 const orderId = crypto.randomUUID();
153
154 yield* db.set(`orders/${orderId}`, { email, total, status: "created" });
155 yield* events.publish({ data: JSON.stringify({ orderId, email, total }) });
156
157 return yield* HttpServerResponse.json({ orderId }, { status: 202 });
158 }).pipe(Effect.orDie),
159 };
160 }).pipe(
161 Effect.provide([GCP.Firestore.ReadWriteDatabaseHttp, GCP.PubSub.WriteTopicHttp]),
162 ),
163) {}
164```
165
166### A queue consumer
167
168The Fulfillment service subscribes to the topic and marks each order
169fulfilled. `consumeTopicMessages` creates the push subscription and the
170route it delivers to:
171
172```typescript
173// src/Fulfillment.ts
174import * as GCP from "alchemy/GCP";
175import * as Effect from "effect/Effect";
176import * as Stream from "effect/Stream";
177import * as HttpServerResponse from "effect/http/HttpServerResponse";
178import { OrderEvents, Orders } from "./resources.ts";
179
180export default class Fulfillment extends GCP.Function<Fulfillment>()(
181 "Fulfillment",
182 { main: import.meta.url, location: "us-central1" },
183 Effect.gen(function* () {
184 const db = yield* GCP.Firestore.WriteDatabase(Orders);
185
186 yield* GCP.PubSub.consumeTopicMessages(OrderEvents, (messages) =>
187 messages.pipe(
188 Stream.runForEach(({ message }) =>
189 Effect.gen(function* () {
190 const { orderId } = JSON.parse(
191 Buffer.from(message.data ?? "", "base64").toString("utf8"),
192 ) as { orderId: string };
193 yield* db.update(`orders/${orderId}`, { status: "fulfilled" });
194 }),
195 ),
196 // A failure answers the push with a 500, so Pub/Sub redelivers.
197 Effect.orDie,
198 ),
199 );
200
201 return { fetch: Effect.succeed(HttpServerResponse.text("ok")) };
202 }).pipe(
203 Effect.provide([GCP.Run.TopicEventSource, GCP.Firestore.WriteDatabaseHttp]),
204 ),
205) {}
206```
207
208Fulfillment has no public URL. Pub/Sub signs each push with an OIDC
209token for the service's own account, which gets `run.invoker` on the
210service, and every request's token is verified before the handler runs.
211Jobs and WorkerPools consume the same topic by pulling instead, with
212`GCP.Run.TopicPullEventSource`.
213
214Add both services to a stack:
215
216```typescript
217// alchemy.run.ts
218import * as Alchemy from "alchemy";
219import * as GCP from "alchemy/GCP";
220import * as Effect from "effect/Effect";
221import Api from "./src/Api.ts";
222import Fulfillment from "./src/Fulfillment.ts";
223
224export default Alchemy.Stack(
225 "Orders",
226 { providers: GCP.providers(), state: Alchemy.localState() },
227 Effect.gen(function* () {
228 const api = yield* Api;
229 yield* Fulfillment;
230 return { url: api.uri };
231 }),
232);
233```
234
235Storage, Pub/Sub, Firestore, and BigQuery have `Read` / `Write` /
236`ReadWrite` clients. `GCP.SQL.Connect` reaches Cloud SQL,
237`GCP.Run.InvokeService` calls another private service with a
238Google-signed ID token, and `GCP.AIPlatform.GenerateContent` calls
239Gemini. Besides Pub/Sub, event sources cover Storage, Cloud Scheduler,
240and Eventarc.
241
242The default region comes from your profile (else `us-central1`), and
243`GCP.Region("europe-west1")` overrides it for a stack. Reconciles wait
244until GCP reports a resource ready, deletes wait until it is gone, and
245`pnpm nuke:gcp` clears a test project. Sixteen examples — including
246`gcp-pubsub-fanout`, `gcp-event-pipeline`, `gcp-cloud-sql-drizzle`,
247`gcp-vertex-ai`, and `gcp-gke` — each pass a live deploy-and-destroy
248test.
249
250Docs: [GCP](/gcp) · [Setup](/gcp/setup) ·
251[Bindings](/gcp/guides/bindings) ·
252[Event pipeline](/gcp/guides/event-pipeline).
253
254## Neon: Functions, storage, Auth, and websites
255
256`alchemy/Neon` grows from Projects and Branches into a full backend
257([#1695](https://github.com/alchemy-run/alchemy/pull/1695)). A
258`Neon.Function` runs a native Fetch handler or an Effect program on
259Node 24, and bindings wire in Postgres and object storage without
260passing your deployment API key to the application:
261
262```typescript
263// src/resources.ts
264export const Backend = Effect.gen(function* () {
265 const project = yield* Neon.Project("Db", {
266 region: "aws-us-east-2",
267 migrations: "./migrations",
268 });
269 const uploads = yield* Neon.Bucket("Uploads", { project, access: "private" });
270 return { project, uploads };
271});
272```
273
274```typescript
275// src/api.ts
276export default class Api extends Neon.Function<Api>()(
277 "Api",
278 Effect.gen(function* () {
279 const { project } = yield* Backend;
280 return { project, main: import.meta.url };
281 }),
282 Effect.gen(function* () {
283 const { project, uploads } = yield* Backend;
284 const db = yield* Neon.Connect(project);
285 const sql = yield* SQL.Postgres({ url: db.connectionString });
286 const files = yield* Neon.WriteBucket(uploads);
287
288 return {
289 fetch: Effect.gen(function* () {
290 const rows = yield* sql`SELECT now() AS time`;
291 yield* files.put("latest.json", JSON.stringify(rows));
292 return yield* HttpServerResponse.json(rows);
293 }).pipe(Effect.orDie),
294 };
295 }).pipe(Effect.provide([Neon.ConnectHttp, Neon.WriteBucketHttp])),
296) {}
297```
298
299Thirteen frameworks deploy as Fetch Functions, with the same props as
300on the other providers:
301
302```diff lang="typescript"
303 // alchemy.run.ts
304 export default Alchemy.Stack(
305 "App",
306 { providers: Neon.providers(), state: Alchemy.localState() },
307 Effect.gen(function* () {
308 const api = yield* Api;
309- const site = yield* Prisma.Website.Astro("Blog", {
310+ const site = yield* Neon.Website.Astro("Blog", {
311 rootDir: "./apps/blog",
312 env: { API_BASE: api.url },
313 });
314 return { url: site.url };
315 }),
316 );
317```
318
319Also in the box:
320
321- **Storage** — `Bucket`, typed `Object<T>` seed values, `Read`/`Write`/`ReadWrite` bucket and object bindings, and presigned GET/PUT URLs.
322- **Auth** — managed Better Auth with `AuthOAuthProvider`, `AuthTrustedDomain`, and a `ConnectAuth` binding for the Auth and JWKS URLs.
323- **Data API** — `DataApi` configures PostgREST; `QueryDataApi` forwards the caller's JWT so row-level security applies.
324- **AI Gateway** — `QueryAIGateway` plus `makeLanguageModelLayer` for Effect AI's `LanguageModel`.
325- **Events** — `CronEventSource` and `BucketEventSource` register a typed handler and its trigger in one call.
326- **Operations** — `Credential`, `CustomDomain`, and experimental organization governance: API keys, member roles, spending limits, and VPC endpoints.
327
328`alchemy dev` runs each website's native framework server and a local
329Fetch runtime for Functions; `.pipe(Alchemy.remote())` deploys one for
330real.
331
332Docs: [Neon](/neon) · [Tutorial](/neon/tutorial) ·
333[Preview branches](/neon/guides/preview-branches) ·
334[Production Auth](/neon/guides/production-auth).
335
336## Secret providers: dotenv, Doppler, and Infisical
337
338A stack now chooses where its secrets come from
339([#1728](https://github.com/alchemy-run/alchemy/pull/1728)) — thanks
340[Rahul Mishra](https://github.com/BlankParticle)! Every
341`Config.Redacted("DATABASE_URL")` in your program — in the stack, in a
342resource provider, or captured by a Worker or Lambda — resolves from the
343list you give `Alchemy.Stack`'s new `secrets` option. Nothing is written
344to `process.env` or to disk.
345
346```typescript
347// alchemy.run.ts
348import * as Alchemy from "alchemy";
349import * as Cloudflare from "alchemy/Cloudflare";
350import * as Doppler from "alchemy/Doppler";
351import * as Effect from "effect/Effect";
352import Api from "./src/api.ts";
353
354export default Alchemy.Stack(
355 "MyApp",
356 {
357 providers: Cloudflare.providers(),
358 state: Cloudflare.state(),
359 secrets: [
360 Doppler.Secrets(({ stage }) => ({
361 project: "my-app",
362 config: stage === "prod" ? "prd" : "dev",
363 })),
364 Alchemy.Secrets.DotEnv(),
365 ],
366 },
367 Effect.gen(function* () {
368 const api = yield* Api;
369 return { url: api.url };
370 }),
371);
372```
373
374The Worker reads the secret the same way it always has:
375
376```typescript
377// src/api.ts
378export default class Api extends Cloudflare.Worker<Api>()(
379 "Api",
380 { main: import.meta.url },
381 Effect.gen(function* () {
382 const databaseUrl = yield* Config.Redacted("DATABASE_URL");
383 const sql = yield* SQL.Postgres({ url: Effect.succeed(databaseUrl) });
384 return {
385 fetch: Effect.gen(function* () {
386 return yield* HttpServerResponse.json(yield* sql`SELECT now() AS time`);
387 }).pipe(Effect.orDie),
388 };
389 }),
390) {}
391```
392
393Rotating the value in Doppler and running `alchemy deploy` now ships it:
394a changed `Config` value read during a Worker's Init plans an update
395([#1837](https://github.com/alchemy-run/alchemy/pull/1837)).
396
397### Precedence
398
399Later entries override earlier ones. A missing key falls through to the
400next source, and an empty string counts as a value. The process
401environment is always the implicit last entry, so a one-off
402`DATABASE_URL=… alchemy deploy` or a CI runner's exported variables
403still win. List `Alchemy.Secrets.ProcessEnv()` yourself to rank it
404elsewhere, or pass `{ disabled: true }` to leave the shell out.
405
406### Per stage
407
408`secrets` and every provider's options also accept a callback that
409receives the stage, so one stack serves `dev`, `pr-42`, and `prod` from
410different places:
411
412```typescript
413secrets: ({ stage }) =>
414 stage === "dev"
415 ? Alchemy.Secrets.DotEnv({ path: [".env", ".env.local"] })
416 : Infisical.Secrets({ project: "my-app", environment: stage, path: "/api" }),
417```
418
419### Logging in, locally and in CI
420
421On your machine, `alchemy profile edit --add Doppler` opens a browser
422login, and `alchemy profile edit --add Infisical` stores a Universal
423Auth machine identity or a user token. In CI, set `DOPPLER_TOKEN` /
424`INFISICAL_TOKEN`, or skip long-lived tokens entirely with OIDC: set
425`DOPPLER_IDENTITY_ID` or `INFISICAL_IDENTITY_ID`, and Alchemy exchanges
426the platform's identity token on GitHub Actions, GitLab CI, Vercel, or
427Google Cloud for a short-lived one on every run:
428
429```yaml
430jobs:
431 deploy:
432 permissions:
433 id-token: write
434 contents: read
435 steps:
436 - run: bun alchemy deploy --stage prod --yes
437 env:
438 DOPPLER_IDENTITY_ID: ${{ vars.DOPPLER_IDENTITY_ID }}
439```
440
441`--log-level debug` prints which variable names each source loaded,
442never their values. Any Effect Layer that provides a `ConfigProvider`
443works as a custom entry.
444
445Docs: [Secret providers](/environments/secret-providers) ·
446[Doppler](/environments/doppler) · [Infisical](/environments/infisical).
447
448## Prisma ORM v8
449
450Prisma ORM v8 runs on an Effect-native Postgres runtime
451([#1198](https://github.com/alchemy-run/alchemy/pull/1198)). Queries,
452prepared statements, streams, and transactions are Effects with typed
453errors and per-execution connection cleanup. Write the contract in
454TypeScript or in Prisma Schema Language (PSL); both use the same
455runtime. Prisma stays an optional peer.
456
457### TypeScript-first
458
459Define the contract with Prisma's own builders:
460
461```typescript
462// src/prisma/contract.ts
463import { defineContract } from "alchemy/Prisma/ORM";
464
465export const contract = defineContract({}, ({ field, model, rel }) => ({
466 models: {
467 User: model("User", {
468 fields: {
469 id: field.id.uuidv7Native(),
470 email: field.text().unique(),
471 name: field.text(),
472 },
473 relations: { posts: rel.hasMany("Post", { by: "authorId" }) },
474 }),
475 Post: model("Post", {
476 fields: {
477 id: field.id.uuidv7Native(),
478 title: field.text(),
479 authorId: field.uuidNative(),
480 },
481 relations: { author: rel.belongsTo("User", { from: "authorId", to: "id" }) },
482 }),
483 },
484}));
485```
486
487Pass it straight to `Postgres` — no generated code:
488
489```typescript
490// src/api.ts
491import * as Cloudflare from "alchemy/Cloudflare";
492import * as PrismaPostgres from "alchemy/Prisma/ORM/Postgres";
493import * as Effect from "effect/Effect";
494import * as HttpServerResponse from "effect/http/HttpServerResponse";
495import { Hyperdrive } from "./db.ts";
496import { contract } from "./prisma/contract.ts";
497
498export default class Api extends Cloudflare.Worker<Api>()(
499 "Api",
500 { main: import.meta.url },
501 Effect.gen(function* () {
502 const conn = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
503 const db = yield* PrismaPostgres.Postgres(conn.connectionString, { contract });
504
505 return {
506 fetch: Effect.gen(function* () {
507 const users = yield* db.orm.public.User.include("posts").all();
508 return yield* HttpServerResponse.json({ users });
509 }).pipe(Effect.orDie),
510 };
511 }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),
512) {}
513```
514
515### PSL-first
516
517Keep `contract.psl` authoritative:
518
519```prisma
520// src/prisma/contract.psl
521model User {
522 id id.uuidv7Native()
523 email String @unique
524 name String
525 posts Post[]
526}
527
528model Post {
529 id id.uuidv7Native()
530 title String
531 authorId Uuid
532 author User @relation(fields: [authorId], references: [id])
533}
534```
535
536Register Alchemy's Effect generator in `prisma.config.ts`, then run
537`alchemy prisma generate` (or `--watch`):
538
539```typescript
540// prisma.config.ts
541import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";
542import { definePrismaConfig } from "prisma/config";
543import { withEffect } from "alchemy/Prisma/ORM/generator";
544
545export default definePrismaConfig({
546 orm: withEffect(
547 ormConfig({
548 contract: "./src/prisma/contract.psl",
549 output: "./src/prisma/generated",
550 }),
551 { client: true, schemas: true },
552 ),
553});
554```
555
556The Worker imports the generated `makeDatabase` instead of the
557contract; queries are identical:
558
559```diff lang="typescript"
560-import * as PrismaPostgres from "alchemy/Prisma/ORM/Postgres";
561-import { contract } from "./prisma/contract.ts";
562+import { makeDatabase } from "./prisma/generated/client.ts";
563
564 Effect.gen(function* () {
565 const conn = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
566- const db = yield* PrismaPostgres.Postgres(conn.connectionString, { contract });
567+ const db = yield* makeDatabase(conn.connectionString);
568```
569
570Both forms can also produce standalone Effect Schemas for rows —
571`makeSchemas(contract)` or the generated `schemas.ts`. The
572`cloudflare-neon-prisma` and `cloudflare-neon-prisma-psl` examples run
573each form end to end on Neon through Hyperdrive, with migrations
574applied on deploy by `Prisma.Contract` and `Prisma.Migrate`.
575
576Docs: [Contracts](/sql/prisma/contracts) ·
577[Postgres](/sql/prisma/postgres) ·
578[Migrations](/sql/prisma/migrations).
579
580## Fly: a Service owns its App
581
582A `Fly.Service` is now its own Fly App with its own public URL
583([#1794](https://github.com/alchemy-run/alchemy/pull/1794)). You no
584longer declare an App and an IP address first:
585
586```diff lang="typescript"
587-export const Site = Fly.App("Site");
588-export const PublicIp = Fly.IpAssignment("Shared", { app: Site, type: "shared_v4" });
589
590 export default class Api extends Fly.Service<Api>()(
591 "Api",
592- { app: Site, main: import.meta.url },
593+ { main: import.meta.url },
594 Effect.gen(function* () {
595 return { fetch: Effect.succeed(HttpServerResponse.text("hello")) };
596 }),
597 ) {}
598```
599
600`public: false` keeps a Service off the internet. A Service can return
601methods beside `fetch`, and another Service binds it with
602`Fly.bindService` for a typed client:
603
604```typescript
605// src/users.ts
606export default class Users extends Fly.Service<Users>()(
607 "Users",
608 { main: import.meta.url, public: false },
609 Effect.gen(function* () {
610 return { list: () => Effect.succeed(USERS) };
611 }),
612) {}
613
614// src/gateway.ts
615export default class Gateway extends Fly.Service<Gateway>()(
616 "Gateway",
617 { main: import.meta.url },
618 Effect.gen(function* () {
619 const users = yield* Fly.bindService(Users);
620 return {
621 fetch: Effect.gen(function* () {
622 return yield* HttpServerResponse.json(yield* users.list());
623 }).pipe(Effect.orDie),
624 };
625 }),
626) {}
627```
628
629Calls are plain HTTP over Flycast. Each one carries the caller's token
630and Fly's signed `Fly-Src` header, so only Services that bind `Users`
631can call it. `network: yield* Fly.stackNetwork` isolates Services on a
632private network per stack and stage, and `region: ["iad", "lhr"]` runs
633`count` Machines in each region behind one hostname.
634
635### Blue/green deploys
636
637Add `deploy: { strategy: "bluegreen" }` and the next `alchemy deploy`
638starts a checked replacement before stopping the running version
639([#1706](https://github.com/alchemy-run/alchemy/pull/1706)).
640`shutdown` gives in-flight responses time to finish:
641
642```diff lang="typescript"
643 export default class Api extends Fly.Service<Api>()(
644 "Api",
645 {
646 main: import.meta.url,
647+ deploy: { strategy: "bluegreen" },
648+ shutdown: { timeout: "60 seconds" },
649 },
650```
651
652Websites take the same two props. An interrupted rollout resumes on the
653next deploy, and a failed readiness check leaves the old version
654serving. Rolling updates stay the default and remain the only option for
655volume-backed Services.
656
657### Multi-container Machines
658
659Run an API and its sidecar in one Machine with Fly's native container
660groups, rolling or blue/green
661([#1768](https://github.com/alchemy-run/alchemy/pull/1768),
662[#1769](https://github.com/alchemy-run/alchemy/pull/1769)) — thanks
663[Florian Bienefelt](https://github.com/Floriferous)!
664
665```typescript
666export const App = Fly.App("Preview");
667
668export const Preview = Fly.Machine("Preview", {
669 app: App,
670 containers: [
671 {
672 name: "api",
673 image: "ghcr.io/acme/api:v3",
674 healthChecks: [{ http: { port: 3000, path: "/health" } }],
675 },
676 {
677 name: "worker",
678 image: "ghcr.io/acme/worker:v3",
679 dependsOn: [{ name: "api", condition: "healthy" }],
680 },
681 ],
682});
683```
684
685Florian also added Flycast `private_v6` addresses to
686`Fly.IpAssignment`
687([#1749](https://github.com/alchemy-run/alchemy/pull/1749)) and made
688rolling updates wait for service checks before touching the next
689replica ([#1670](https://github.com/alchemy-run/alchemy/pull/1670)).
690Fly bindings now carry their values through the runtime context
691([#1554](https://github.com/alchemy-run/alchemy/pull/1554)) — thanks
692[Michael K](https://github.com/Mkassabov)!
693
694Docs: [Services](/fly/compute/services) ·
695[Connecting Services](/fly/compute/connecting-services) ·
696[Deployments](/fly/compute/deployments) ·
697[Networking](/fly/networking).
698
699## Deploy part of a stack
700
701`--include` and `--exclude` reconcile a subset of resources and Actions
702([#1757](https://github.com/alchemy-run/alchemy/pull/1757)). Each flag
703takes an FQN, a logical ID, or a glob, and can repeat:
704
705```sh
706alchemy deploy --stage pr-123 --include 'Database/**'
707alchemy dev --exclude 'Services/Background/**'
708alchemy plan --include 'Database/Branch' --include 'Database/Password'
709```
710
711Dependencies of included resources come along automatically; excluding
712one fails the plan with its dependency chain. Excluded state is left
713untouched. `Test.make`'s `deploy` takes the same filters:
714
715```typescript
716const { test, deploy } = Test.make({ providers: Cloudflare.providers() });
717
718test(
719 "migrates the database",
720 Effect.gen(function* () {
721 yield* deploy(Stack, { include: ["Database/**"] });
722 }),
723);
724```
725
726## Durable callbacks
727
728`Alchemy.makeCallback` schedules typed work that survives the request
729([#1668](https://github.com/alchemy-run/alchemy/pull/1668)). Durable
730Objects are the first host: jobs live in SQLite and fire from native
731alarms, and scheduling joins the storage transaction, so the write and
732the job commit together:
733
734```typescript
735export default class Document extends Cloudflare.DurableObject<Document>()(
736 "Document",
737 Effect.gen(function* () {
738 const state = yield* Cloudflare.DurableObjectState;
739 return Effect.gen(function* () {
740 const onSnapshot = yield* Alchemy.makeCallback(
741 "snapshot",
742 Effect.fn(function* (payload: { revision: string; body: string }) {
743 yield* state.storage.put(`revisions:${payload.revision}`, payload.body);
744 }),
745 );
746
747 return {
748 save: Effect.fn(function* (revision: string, body: string) {
749 yield* state.storage.transaction(
750 Effect.gen(function* () {
751 yield* state.storage.put("document", { revision, body });
752 yield* onSnapshot.schedule(revision, {
753 after: "30 seconds",
754 payload: { revision, body },
755 });
756 }),
757 );
758 }),
759 };
760 });
761 }),
762) {}
763```
764
765Delivery is at least once with retries. Existing `scheduleEvent` jobs
766keep running beside callbacks on the object's single alarm. Alarm
767updates are now batched once per transaction
768([#1701](https://github.com/alchemy-run/alchemy/pull/1701)).
769
770Docs: [Durable Objects → Register a durable callback](/cloudflare/compute/durable-objects#register-a-durable-callback).
771
772## Hibernating WebSocket RPC
773
774An `RpcDurableObject` that returns its handler Layer now serves Effect
775RPC over hibernating WebSockets as well as HTTP
776([#1705](https://github.com/alchemy-run/alchemy/pull/1705)). The
777request picks the transport; your code doesn't change.
778
779Define the RPCs once and share them between server and client:
780
781```typescript
782// rpcs.ts
783import * as Schema from "effect/Schema";
784import { Rpc, RpcGroup } from "effect/rpc";
785
786export class CounterRpcs extends RpcGroup.make(
787 Rpc.make("increment", { payload: {}, success: Schema.Number }),
788) {}
789```
790
791Implement them in an `RpcDurableObject`:
792
793```typescript
794// counter.ts
795import * as Cloudflare from "alchemy/Cloudflare";
796import * as Effect from "effect/Effect";
797import { CounterRpcs } from "./rpcs.ts";
798
799export default class Counter extends Cloudflare.RpcDurableObject<Counter>()(
800 "Counter",
801 { schema: CounterRpcs },
802 Effect.gen(function* () {
803 const state = yield* Cloudflare.DurableObjectState;
804 return Effect.gen(function* () {
805 return CounterRpcs.toLayer({
806 increment: () =>
807 state.storage.transaction(
808 state.storage.get<number>("count").pipe(
809 Effect.map((n) => (n ?? 0) + 1),
810 Effect.tap((n) => state.storage.put("count", n)),
811 ),
812 ).pipe(Effect.orDie),
813 });
814 });
815 }),
816) {}
817```
818
819The Worker picks an object by name and forwards the connection to it:
820
821```typescript
822// worker.ts
823import * as Cloudflare from "alchemy/Cloudflare";
824import * as Effect from "effect/Effect";
825import { HttpServerRequest } from "effect/http/HttpServerRequest";
826import * as HttpServerResponse from "effect/http/HttpServerResponse";
827import Counter from "./counter.ts";
828
829export default class CounterWorker extends Cloudflare.Worker<CounterWorker>()(
830 "CounterWorker",
831 { main: import.meta.url },
832 Effect.gen(function* () {
833 const counters = yield* Counter;
834 return {
835 fetch: Effect.gen(function* () {
836 const request = yield* HttpServerRequest;
837 const path = new URL(request.url, "https://worker").pathname;
838 const name = /^\/counters\/([\w-]+)$/.exec(path)?.[1];
839 if (!name) return HttpServerResponse.empty({ status: 404 });
840 return yield* counters.fetch(name, request);
841 }),
842 };
843 }),
844) {}
845```
846
847In the browser, `alchemy/Cloudflare/RpcWebSocketClient` builds a typed
848client over one WebSocket. It imports only Effect modules, so it is safe
849to bundle:
850
851```typescript
852// client.ts
853import * as RpcWebSocketClient from "alchemy/Cloudflare/RpcWebSocketClient";
854import * as Context from "effect/Context";
855import * as Effect from "effect/Effect";
856import * as Layer from "effect/Layer";
857import * as RpcClient from "effect/rpc/RpcClient";
858import type { RpcClientError } from "effect/rpc/RpcClientError";
859import * as Socket from "effect/socket/Socket";
860import { CounterRpcs } from "./rpcs.ts";
861
862class CounterClient extends Context.Service<
863 CounterClient,
864 RpcClient.FromGroup<typeof CounterRpcs, RpcClientError>
865>()("CounterClient") {}
866
867const CounterClientLive = RpcWebSocketClient.layer(
868 CounterClient,
869 CounterRpcs,
870 "wss://example.com/counters/alice",
871).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
872
873const program = Effect.gen(function* () {
874 const counter = yield* CounterClient;
875 return yield* counter.increment({});
876}).pipe(Effect.provide(CounterClientLive));
877```
878
879Every call on that socket goes to the `"alice"` object. While the
880connection is idle the Durable Object can hibernate; the next call wakes
881it and is answered on the same socket. Alchemy handles the heartbeats and
882restores the connection after a wake. In-flight calls are not replayed:
883if the object restarts mid-call, the socket closes with code `1012`.
884
885Docs: [Effect RPC → HTTP and WebSocket RPC](/cloudflare/apis/effect-rpc#http-and-websocket-rpc)
886· [Forward browser connections](/cloudflare/apis/effect-rpc#forward-browser-connections).
887
888## Typed WebSocket attachments
889
890If you write your own WebSocket handlers on a plain `DurableObject`
891instead of using RPC, you can now validate the data you attach to each
892socket with an Effect Schema
893([#1723](https://github.com/alchemy-run/alchemy/pull/1723)). A session
894read back after hibernation comes back decoded and typed, where
895`deserializeAttachment` used to return an unchecked value:
896
897```typescript
898// room.ts
899import * as Cloudflare from "alchemy/Cloudflare";
900import * as Effect from "effect/Effect";
901import * as Schema from "effect/Schema";
902
903const Session = Schema.Struct({
904 id: Schema.String,
905 joinedAt: Schema.DateFromString,
906});
907
908export default class Room extends Cloudflare.DurableObject<Room>()(
909 "Rooms",
910 Effect.gen(function* () {
911 return Effect.gen(function* () {
912 return {
913 fetch: Effect.gen(function* () {
914 const [response, socket] = yield* Cloudflare.upgrade();
915 const joinedAt = yield* Effect.sync(() => new Date());
916 yield* socket.setAttachment(Session, { id: crypto.randomUUID(), joinedAt });
917 return response;
918 }),
919 webSocketMessage: Effect.fn(function* (socket: Cloudflare.WebSocket) {
920 const session = yield* socket.getAttachment(Session); // joinedAt: Date
921 yield* socket.send(`joined ${session.joinedAt.toISOString()}`);
922 }),
923 };
924 });
925 }),
926) {}
927```
928
929A missing or invalid attachment fails with a typed
930`WebSocketAttachmentError`, so you decide whether to close the socket or
931ignore it.
932
933Docs: [Hibernatable WebSockets → Validate attachments after hibernation](/cloudflare/compute/hibernatable-websockets#validate-attachments-after-hibernation).
934
935## Presigned R2 URLs in `alchemy dev`
936
937Presigned R2 URLs and S3 clients work under `alchemy dev` with no extra
938configuration ([#1793](https://github.com/alchemy-run/alchemy/pull/1793)).
939The same code signs against the Worker's local dev URL in dev and
940against `r2.cloudflarestorage.com` when deployed:
941
942```typescript
943export const Uploads = Cloudflare.R2.Bucket("Uploads");
944
945export default Cloudflare.Worker(
946 "Api",
947 { main: import.meta.url },
948 Effect.gen(function* () {
949 const presignPut = yield* Cloudflare.R2.PresignPutObject(Uploads);
950 return {
951 fetch: Effect.gen(function* () {
952 const url = yield* presignPut({ key: "uploads/a.png", contentType: "image/png" });
953 return yield* HttpServerResponse.json({ url });
954 }).pipe(Effect.orDie),
955 };
956 }).pipe(Effect.provide(Cloudflare.R2.PresignPutObjectToken)),
957);
958```
959
960Async Workers get the same thing through
961`Cloudflare.R2.S3Credentials(bucket, { access: "write" })`, which
962delivers an endpoint and scoped keys for any S3 client. The local S3
963endpoint runs inside the Worker's existing workerd process, and the dev
964proxy now answers `Expect: 100-continue`, which took a 3 MB AWS SDK
965`PutObject` from ~6 s to ~130 ms.
966
967Docs: [R2 presigned URLs](/cloudflare/data/r2-presigned-urls).
968
969## Queue subscriptions from any resource
970
971`Cloudflare.Queues.Subscription` accepts a resource or a `.ref(...)` as
972its `source` ([#1722](https://github.com/alchemy-run/alchemy/pull/1722),
973[#1505](https://github.com/alchemy-run/alchemy/pull/1505)) — including
974one owned by another stack:
975
976```typescript
977export const BucketEvents = Effect.gen(function* () {
978 const queue = yield* Cloudflare.Queues.Queue("Events");
979 const uploads = yield* Cloudflare.R2.Bucket.ref("Uploads", {
980 stack: "storage",
981 stage: "prod",
982 });
983 return yield* Cloudflare.Queues.Subscription("BucketEvents", {
984 source: uploads,
985 events: ["bucket.created", "bucket.deleted"],
986 queueId: queue.queueId,
987 });
988});
989```
990
991Sources include Images, KV, R2, Vectorize, Workers, Workflows, and two
992new resources: `AI.Model`, a handle on a Workers AI catalog model, and
993`R2.SuperSlurperJob`, a one-off migration into R2 from S3 or another
994bucket. Thanks [Alex](https://github.com/agcty) for the Workflow
995sources and `Cloudflare.Workflow.ref`!
996
997Docs: [Queues → Use resources as event sources](/cloudflare/messaging/queues#use-resources-as-event-sources).
998
999## ACME certificates
1000
1001`alchemy/ACME` issues and renews certificates from Let's Encrypt or any
1002ACME CA, independent of where they are used
1003([#1702](https://github.com/alchemy-run/alchemy/pull/1702)).
1004`Cloudflare.DNS.AcmeSolver` answers DNS-01 challenges:
1005
1006```typescript
1007export const Site = Fly.App("Site");
1008
1009export const Wildcard = Effect.gen(function* () {
1010 const account = yield* ACME.Account("LetsEncrypt", {
1011 ca: ACME.LetsEncrypt,
1012 termsOfServiceAgreed: true,
1013 });
1014 const certificate = yield* ACME.Certificate("Wildcard", {
1015 account,
1016 identifiers: ["*.example.com"],
1017 solver: Cloudflare.DNS.AcmeSolver({
1018 zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
1019 }),
1020 });
1021 return yield* Fly.Certificate("Upload", {
1022 app: Site,
1023 hostname: "*.example.com",
1024 kind: "custom",
1025 fullchain: certificate.chain,
1026 privateKey: certificate.privateKey,
1027 });
1028});
1029```
1030
1031`ACME.IssueCertificate` binds an account into a Worker or Service for
1032runtime issuance, and `Fly.WriteCertificates` manages an App's
1033certificates at runtime.
1034
1035Docs: [ACME](/acme) · [Getting started](/acme/getting-started).
1036
1037## Kubernetes on any cluster
1038
1039`Kubernetes.LocalCluster` starts a kind cluster with a local image
1040registry, and any connection can now name a registry, so workloads
1041built from `main` or a Docker context deploy to GKE, AKS, k3s, or your
1042laptop ([#1779](https://github.com/alchemy-run/alchemy/pull/1779)):
1043
1044```typescript
1045export const Cluster = Effect.gen(function* () {
1046 const stage = yield* Alchemy.Stage;
1047 if (stage === "prod") {
1048 // any cluster kubectl can reach
1049 return Kubernetes.KubeConfig({
1050 context: "prod",
1051 registry: { server: "ghcr.io/acme" },
1052 architecture: "arm64",
1053 });
1054 }
1055 return yield* Kubernetes.LocalCluster("Cluster", { name: "alchemy" });
1056});
1057```
1058
1059Editing a `Job` or `Deployment`'s `main` program now redeploys it. A
1060new `/kubernetes` docs hub has a five-part tutorial that starts on
1061`LocalCluster` and ends on your own cluster.
1062
1063Docs: [Kubernetes](/kubernetes) · [Tutorial](/kubernetes/tutorial/part-1)
1064· [Local cluster](/kubernetes/clusters/local).
1065
1066## Workflow tasks keep typed errors
1067
1068`Cloudflare.Workflows.task` accepts fallible Effects
1069([#1723](https://github.com/alchemy-run/alchemy/pull/1723)). A failure
1070follows the task's retry policy; once retries run out, `catchTag` sees
1071your error, including after Cloudflare replays the step:
1072
1073```typescript
1074class CardDeclined extends Data.TaggedError("CardDeclined")<{
1075 reason: string;
1076}> {}
1077
1078const chargeCard = (orderId: string) =>
1079 Effect.fail(new CardDeclined({ reason: `insufficient funds for ${orderId}` }));
1080
1081export default class Checkout extends Cloudflare.Workflow<Checkout>()(
1082 "Checkout",
1083 Effect.gen(function* () {
1084 return Effect.fn(function* (input: { orderId: string }) {
1085 return yield* Cloudflare.Workflows.task("charge", chargeCard(input.orderId), {
1086 retries: { limit: 2, delay: "1 second", backoff: "exponential" },
1087 }).pipe(
1088 Effect.catchTag("CardDeclined", (error) =>
1089 Effect.succeed({ status: "declined" as const, reason: error.reason }),
1090 ),
1091 );
1092 });
1093 }),
1094) {}
1095```
1096
1097`Effect.orDie` still makes a failure terminal. Workflows can also take
1098an explicit account-global `workflowName`
1099([#1460](https://github.com/alchemy-run/alchemy/pull/1460)) — thanks
1100[Odysseas Papadimas](https://github.com/odysseaspapadimas)! A
1101retries-only step config no longer crashes the engine
1102([#1142](https://github.com/alchemy-run/alchemy/pull/1142)) — thanks
1103[apostoli](https://github.com/apostolos-geyer)! Each attempt gets its
1104own scope ([#1701](https://github.com/alchemy-run/alchemy/pull/1701)).
1105
1106Docs: [Workflows → Application errors across replay](/cloudflare/compute/workflows#application-errors-across-replay).
1107
1108## Vinext support
1109
1110`Website.Vinext` deploys [vinext](https://github.com/cloudflare/vinext)
1111apps on Cloudflare, AWS, Fly, Hetzner, Railway, and Prisma
1112([#1515](https://github.com/alchemy-run/alchemy/pull/1515)) — thanks
1113[Ray](https://github.com/xesrevinu)! Keep your ordinary
1114`vinext()` Vite config; no Alchemy plugin is needed.
1115
1116```typescript
1117export default Alchemy.Stack(
1118 "Web",
1119 { providers: Cloudflare.providers(), state: Cloudflare.state() },
1120 Effect.gen(function* () {
1121 const site = yield* Cloudflare.Website.Vinext("Web", {
1122 rootDir: "./app",
1123 env: { GREETING: "Hello from vinext!" },
1124 });
1125 return { url: site.url };
1126 }),
1127);
1128```
1129
1130On Cloudflare it deploys a Worker with KV caching and prerender seeding,
1131without Wrangler or OpenNext. AWS runs it on streaming Lambda with an S3
1132cache behind CloudFront. Fly, Hetzner, Railway, and Prisma run vinext's
1133Node server, with an in-memory cache by default and Redis through
1134`REDIS_URL`. Swap the provider namespace to move the same app:
1135
1136```diff lang="typescript"
1137 export default Alchemy.Stack(
1138 "Web",
1139- { providers: Cloudflare.providers(), state: Cloudflare.state() },
1140+ { providers: AWS.providers(), state: AWS.state() },
1141 Effect.gen(function* () {
1142- const site = yield* Cloudflare.Website.Vinext("Web", {
1143+ const site = yield* AWS.Website.Vinext("Web", {
1144 rootDir: "./app",
1145 env: { GREETING: "Hello from vinext!" },
1146 });
1147 return { url: site.url };
1148 }),
1149 );
1150```
1151
1152`alchemy dev` runs vinext's native dev server with HMR, and
1153`.pipe(Alchemy.remote())` deploys it for real.
1154
1155Docs: [Cloudflare](/cloudflare/frontend/vinext) · [AWS](/aws/frontend/vinext) ·
1156[Fly](/fly/frontend/vinext) · [Hetzner](/hetzner/frontend/vinext) ·
1157[Railway](/railway/frontend/vinext) · [Prisma](/prisma/frontend/vinext).
1158
1159## Next.js and Octane with less config
1160
1161Next.js on Cloudflare no longer needs `open-next.config.ts`, and
1162writable ISR is one prop
1163([#1747](https://github.com/alchemy-run/alchemy/pull/1747)):
1164
1165```diff lang="typescript"
1166 export const IncCache = Cloudflare.KV.Namespace("NextIncCache");
1167 export const TagCache = Cloudflare.KV.Namespace("NextTagCache");
1168
1169 export const Website = Cloudflare.Website.Nextjs("Website", {
1170- env: {
1171- NEXT_INC_CACHE_KV: IncCache,
1172- NEXT_TAG_CACHE_KV: TagCache,
1173- NEXT_CACHE_DO_QUEUE: Cloudflare.DurableObject("NEXT_CACHE_DO_QUEUE", {
1174- className: "DOQueueHandler",
1175- }),
1176- },
1177+ isr: { incrementalCache: IncCache, tagCache: TagCache },
1178 });
1179```
1180
1181Octane apps no longer need a hosting adapter in `octane.config.ts` on
1182any provider ([#1747](https://github.com/alchemy-run/alchemy/pull/1747),
1183[#1755](https://github.com/alchemy-run/alchemy/pull/1755),
1184[#1756](https://github.com/alchemy-run/alchemy/pull/1756)). SvelteKit
1185moves to `3.0.0-next.27`, and the Node target now ships client assets
1186and prerendered pages
1187([#1709](https://github.com/alchemy-run/alchemy/pull/1709)) — thanks
1188[Cameron McEvenue](https://github.com/cam-mcevenue)! The Next.js Node
1189and Neon servers start from the built config, so a read-only deploy no
1190longer tries to download SWC
1191([#1805](https://github.com/alchemy-run/alchemy/pull/1805)) — thanks
1192[Aman Varshney](https://github.com/AmanVarshney01)!
1193
1194Docs: [Frontend frameworks](/cloudflare/frontend/frontends) ·
1195[Next.js](/cloudflare/frontend/nextjs) · [Octane](/cloudflare/frontend/octane).
1196
1197## Local development
1198
1199- **No credentials needed for local-only stacks**
1200 ([#1801](https://github.com/alchemy-run/alchemy/pull/1801)). Cloudflare
1201 authentication is deferred until a cloud call needs it, so a stack of
1202 local Workers, KV, R2, and D1 runs without a profile.
1203- **Provider credentials resolve on first use**
1204 ([#1902](https://github.com/alchemy-run/alchemy/pull/1902)). Loading
1205 provider layers no longer requires credentials for every provider.
1206 When a cloud operation needs missing or unreadable credentials, plan
1207 and apply fail with `CredentialsRequired`.
1208- **workerd restarts after a crash**
1209 ([#1772](https://github.com/alchemy-run/alchemy/pull/1772)) on the
1210 same ports, so the dev URL keeps working. Raise the isolate's heap with
1211 `ALCHEMY_WORKERD_V8_FLAGS=--max-old-space-size=4096`
1212 ([#1771](https://github.com/alchemy-run/alchemy/pull/1771)), and the
1213 Vite module runner no longer retains every old module graph across HMR
1214 edits ([#1770](https://github.com/alchemy-run/alchemy/pull/1770)).
1215 Thanks [Alex](https://github.com/agcty)!
1216- **Private Websites answer service-binding calls in dev**
1217 ([#1798](https://github.com/alchemy-run/alchemy/pull/1798)) for
1218 SvelteKit, Nuxt, Octane, Next.js, Astro, Waku, and Vocs, and a Vite
1219 Worker with SPA fallback plus `runWorkerFirst` starts again
1220 ([#1799](https://github.com/alchemy-run/alchemy/pull/1799)). Thanks
1221 [Rahul Mishra](https://github.com/BlankParticle)!
1222
1223## Also in this release
1224
1225- **Provider requirements retain their types**
1226 ([#1895](https://github.com/alchemy-run/alchemy/pull/1895)) — fixes
1227 prevent `any` and `unknown` from hiding required services in provider
1228 layers, with stronger validation for provider implementations.
1229- **Provider and runtime fixes for Effect stable**
1230 ([#1899](https://github.com/alchemy-run/alchemy/pull/1899)) — Worker
1231 preview creation reuses an existing preview after a conflict, and
1232 lifecycle fixes cover ACME, CloudFront KeyValueStore, ECS, IAM SAML
1233 providers, IVS stream keys, Cloudflare AI evaluations, and Railway.
1234
1235- **AWS Client VPN**
1236 ([#1714](https://github.com/alchemy-run/alchemy/pull/1714)) —
1237 `ClientVpnEndpoint`, `ClientVpnTargetNetworkAssociation`,
1238 `ClientVpnAuthorizationRule`, and `ClientVpnRoute`.
1239- **`AWS.EC2.DefaultSecurityGroup`**
1240 ([#1630](https://github.com/alchemy-run/alchemy/pull/1630)) manages
1241 the rules on a VPC's default security group, and ECS Services deploy
1242 again after a failed rollout
1243 ([#1650](https://github.com/alchemy-run/alchemy/pull/1650)). Thanks
1244 [Henning Pokriefke](https://github.com/henningpokriefke)!
1245- **RDS master secret policy and security drift** —
1246 `masterUserSecretResourcePolicy` on `DBInstance`
1247 ([#1598](https://github.com/alchemy-run/alchemy/pull/1598)), security
1248 settings converge from desired state
1249 ([#1599](https://github.com/alchemy-run/alchemy/pull/1599)), and a
1250 stopped or failed instance fails fast with
1251 `DBInstanceReadinessBlocked`
1252 ([#1597](https://github.com/alchemy-run/alchemy/pull/1597)). Thanks
1253 [Bjorn Pagen](https://github.com/bjornpagen)!
1254- **Least-privilege AWS bindings**
1255 ([#1357](https://github.com/alchemy-run/alchemy/pull/1357)) — exact
1256 IAM actions per capability, versioned S3 reads and copies, and S3
1257 notification reconciliation across Lambda, SQS, SNS, and EventBridge.
1258 SQS queues also reconcile adoption and Standard ↔ FIFO changes. Thanks
1259 [Saatvik Arya](https://github.com/aryasaatvik)!
1260- **`Cloudflare.Access.McpServer`**
1261 ([#1658](https://github.com/alchemy-run/alchemy/pull/1658)) registers
1262 upstream MCP servers with Access. Thanks
1263 [Dillion Verma](https://github.com/dillionverma)!
1264- **Container images shared across stages**
1265 ([#1692](https://github.com/alchemy-run/alchemy/pull/1692)) —
1266 Container builds are cached by default, and
1267 `publish: { repository: "web" }` reuses images and layers across
1268 stages. Thanks [Dan van der Merwe](https://github.com/danieljvdm)!
1269- **`Cloudflare.Container.ref`**
1270 ([#1750](https://github.com/alchemy-run/alchemy/pull/1750)) — another
1271 stack can read a container application like any other resource.
1272 Thanks [Cyberistic](https://github.com/Cyberistic)!
1273- **Durable Object SQL migrations load at construction**
1274 ([#1736](https://github.com/alchemy-run/alchemy/pull/1736)) —
1275 `yield* Cloudflare.SqlMigrations("./drizzle")` replaces importing
1276 drizzle-kit's `migrations.js`, and each migration commits atomically
1277 with its history row.
1278- **Worker build input options**
1279 ([#1700](https://github.com/alchemy-run/alchemy/pull/1700)) —
1280 `build.input` passes Rolldown input options such as `resolve.alias`.
1281- **Cloudflare fixes** — full deploys keep `version.tag` and
1282 `version.message`
1283 ([#1754](https://github.com/alchemy-run/alchemy/pull/1754), thanks
1284 [Luke O'Malley](https://github.com/lukeomalley)!), prebuilt Workers
1285 keep explicit compatibility flags
1286 ([#1725](https://github.com/alchemy-run/alchemy/pull/1725)),
1287 Workers-for-Platforms uploads keep `_headers` and `_redirects`
1288 ([#1802](https://github.com/alchemy-run/alchemy/pull/1802)), and DNS
1289 records only treat a missing record as gone
1290 ([#1800](https://github.com/alchemy-run/alchemy/pull/1800)).
1291- **Engine fixes** — completed Actions reuse their outputs when inputs
1292 are unchanged, so downstream resources plan as no-ops
1293 ([#1379](https://github.com/alchemy-run/alchemy/pull/1379), thanks
1294 [Leonardo E. Dominguez](https://github.com/focux)!); changing a
1295 `Config` value read during a Worker's Init now plans an update
1296 ([#1837](https://github.com/alchemy-run/alchemy/pull/1837)); and
1297 destroy cleans up every generation left by an interrupted replacement
1298 ([#1704](https://github.com/alchemy-run/alchemy/pull/1704)).
1299- **`alchemy plan --adopt`**
1300 ([#1811](https://github.com/alchemy-run/alchemy/pull/1811)). Thanks
1301 [Pedro Toledo](https://github.com/pltoledo)!
1302- **Railway `region` places replicas**
1303 ([#1766](https://github.com/alchemy-run/alchemy/pull/1766)) on
1304 `Service` and `Function`.
1305- **PlanetScale `withReplication`**
1306 ([#1777](https://github.com/alchemy-run/alchemy/pull/1777)) on
1307 `PostgresRole` for logical-replication consumers. Thanks
1308 [Makisuo](https://github.com/Makisuo)!
1309- **Better Auth tutorial**
1310 ([#1732](https://github.com/alchemy-run/alchemy/pull/1732)) — a
1311 six-part walkthrough from an Auth service to GitHub sign-in, adapted
1312 from work by [xofromthemoon](https://github.com/xofromthemoon).
1313- **Prisma fixes** — dev commands default to `NODE_ENV=development`
1314 ([#1703](https://github.com/alchemy-run/alchemy/pull/1703)) and
1315 `alchemy/Prisma`, Fly, Hetzner, Railway, and Neon import without
1316 `@alchemy.run/frontend-frameworks` installed
1317 ([#1815](https://github.com/alchemy-run/alchemy/pull/1815)). Thanks
1318 [Aman Varshney](https://github.com/AmanVarshney01), and
1319 [Kristof Siket](https://github.com/kristof-siket) for live branch
1320 attachment coverage
1321 ([#1359](https://github.com/alchemy-run/alchemy/pull/1359))!
1322- **Smaller installs** — `@effect/vitest` is an optional peer again
1323 ([#1816](https://github.com/alchemy-run/alchemy/pull/1816)), and
1324 `@aws-sdk/credential-providers`, `aws4fetch`, and `undici` are gone
1325 ([#1665](https://github.com/alchemy-run/alchemy/pull/1665), thanks
1326 [Michael K](https://github.com/Mkassabov)!).
1327- **Runtime directories and process cleanup** — a configured
1328 `.alchemy` directory is honored everywhere
1329 ([#1726](https://github.com/alchemy-run/alchemy/pull/1726)), and
1330 `Command` wrappers get `SIGTERM` and a second to clean up
1331 ([#1760](https://github.com/alchemy-run/alchemy/pull/1760)).
1332- **Declaration metadata**
1333 ([#1490](https://github.com/alchemy-run/alchemy/pull/1490)) —
1334 `Api.LogicalId` keeps its literal type, and Stack references expose
1335 `stackName`.
1336- **Leaner preview packages**
1337 ([#1727](https://github.com/alchemy-run/alchemy/pull/1727),
1338 [#1681](https://github.com/alchemy-run/alchemy/pull/1681)) — a PR
1339 publishes only the packages it affects plus their dependencies, and
1340 fork PRs install from `pkg.alchemy.run/alchemy/pr:<number>:<sha>`. The
1341 CLI ships plain JavaScript entrypoints with generated package exports
1342 ([#1707](https://github.com/alchemy-run/alchemy/pull/1707)). Thanks
1343 [Rahul Mishra](https://github.com/BlankParticle)!
1344- **Test runner tags and plans**
1345 ([#1773](https://github.com/alchemy-run/alchemy/pull/1773)) —
1346 `alchemy-test` gains `--tags` expressions, phased `--plan` runs, and a
1347 `--dry-run` preview.
1348- **Faster API reference**
1349 ([#1767](https://github.com/alchemy-run/alchemy/pull/1767),
1350 [#1774](https://github.com/alchemy-run/alchemy/pull/1774)) — one page
1351 per service: 4,368 pages become 406, the docs build is 4× faster, and
1352 old URLs redirect.
1353- **SQL guides**
1354 ([#1737](https://github.com/alchemy-run/alchemy/pull/1737),
1355 [#1744](https://github.com/alchemy-run/alchemy/pull/1744)) — a
1356 database comparison and deployment guides for AWS (including Aurora
1357 and DSQL), Fly, Hetzner, Prisma, and Railway.
1358- **A new landing page**
1359 ([#1775](https://github.com/alchemy-run/alchemy/pull/1775),
1360 [#1797](https://github.com/alchemy-run/alchemy/pull/1797)) built
1361 around the dev → test → review → deploy loop, with a PR lifecycle
1362 benchmark you can run from `examples/cloudflare-preview-benchmark`.
1363
1364## Where to go next
1365
1366- [GCP](/gcp) · [Setup](/gcp/setup) · [Bindings](/gcp/guides/bindings)
1367- [Neon](/neon) · [Tutorial](/neon/tutorial)
1368- [Secret providers](/environments/secret-providers) · [Doppler](/environments/doppler) · [Infisical](/environments/infisical)
1369- [Prisma ORM contracts](/sql/prisma/contracts)
1370- [Fly Services](/fly/compute/services) · [Connecting Services](/fly/compute/connecting-services) · [Deployments](/fly/compute/deployments)
1371- [Durable Objects](/cloudflare/compute/durable-objects) · [Effect RPC](/cloudflare/apis/effect-rpc)
1372- [R2 presigned URLs](/cloudflare/data/r2-presigned-urls)
1373- [ACME](/acme) · [Kubernetes](/kubernetes)
1374- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta80)
1375- [Compare v2.0.0-beta.79 → v2.0.0-beta.80](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.79...v2.0.0-beta.80)