| 1 | --- |
| 2 | title: 2.0.0-beta.80 - GCP, Neon & Secret Providers |
| 3 | date: 2026-09-29T09:00:00Z |
| 4 | category: release |
| 5 | excerpt: 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 | |
| 8 | beta.80 has three headline additions. **Google Cloud** arrives with |
| 9 | about 1,050 resources across 190 services, Effect-native runtimes on |
| 10 | Cloud Run, Cloud Functions, and GKE, and bindings that grant each host |
| 11 | the narrowest IAM role GCP allows. **Neon** becomes a full backend |
| 12 | provider: Functions, object storage, managed Auth, the Data API, AI |
| 13 | Gateway, and 13 `Neon.Website` frameworks, all in one stack. **Secret |
| 14 | providers** let a stack load its `Config` from dotenv files, |
| 15 | **Doppler**, or **Infisical**, per stage, with browser login on your |
| 16 | machine 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 |
| 20 | other Services, blue/green deploys, and multi-container Machines, and |
| 21 | **`--include` / `--exclude`** deploy a slice of a stack. On Cloudflare: |
| 22 | durable 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 |
| 82 | resources across 190 services, with Effect-native runtimes on every GCP |
| 83 | compute 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 | |
| 93 | Bindings follow the same shape as AWS and Cloudflare. Each `yield*` |
| 94 | grants one role to the host's runtime service account, scoped as tightly |
| 95 | as GCP allows, and the grant is revoked when the binding is removed. |
| 96 | |
| 97 | Here is an order service: a Cloud Run API that stores orders in |
| 98 | Firestore and publishes an event, and a second service that consumes |
| 99 | those events from Pub/Sub. Start with the database and the queue: |
| 100 | |
| 101 | ```typescript |
| 102 | // src/resources.ts |
| 103 | import * as GCP from "alchemy/GCP"; |
| 104 | |
| 105 | export const Orders = GCP.Firestore.Database("Orders", { |
| 106 | location: "us-central1", |
| 107 | type: "FIRESTORE_NATIVE", |
| 108 | }); |
| 109 | |
| 110 | export 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 |
| 117 | Next.js, Nuxt, Astro, SvelteKit, TanStack Start, React Router, SolidStart, |
| 118 | Waku, Vite, Vocs, Vinext, Foldkit, and Octane. The shared options cover |
| 119 | public access, ingress, scaling, and container resources. |
| 120 | |
| 121 | ```typescript |
| 122 | const site = yield* GCP.Website.Nextjs("Web", { |
| 123 | rootDir: "./web", |
| 124 | }); |
| 125 | ``` |
| 126 | |
| 127 | ### A database |
| 128 | |
| 129 | The API writes each order to Firestore and announces it on the topic: |
| 130 | |
| 131 | ```typescript |
| 132 | // src/Api.ts |
| 133 | import * as GCP from "alchemy/GCP"; |
| 134 | import * as Effect from "effect/Effect"; |
| 135 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 136 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 137 | import { OrderEvents, Orders } from "./resources.ts"; |
| 138 | |
| 139 | export 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 | |
| 168 | The Fulfillment service subscribes to the topic and marks each order |
| 169 | fulfilled. `consumeTopicMessages` creates the push subscription and the |
| 170 | route it delivers to: |
| 171 | |
| 172 | ```typescript |
| 173 | // src/Fulfillment.ts |
| 174 | import * as GCP from "alchemy/GCP"; |
| 175 | import * as Effect from "effect/Effect"; |
| 176 | import * as Stream from "effect/Stream"; |
| 177 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 178 | import { OrderEvents, Orders } from "./resources.ts"; |
| 179 | |
| 180 | export 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 | |
| 208 | Fulfillment has no public URL. Pub/Sub signs each push with an OIDC |
| 209 | token for the service's own account, which gets `run.invoker` on the |
| 210 | service, and every request's token is verified before the handler runs. |
| 211 | Jobs and WorkerPools consume the same topic by pulling instead, with |
| 212 | `GCP.Run.TopicPullEventSource`. |
| 213 | |
| 214 | Add both services to a stack: |
| 215 | |
| 216 | ```typescript |
| 217 | // alchemy.run.ts |
| 218 | import * as Alchemy from "alchemy"; |
| 219 | import * as GCP from "alchemy/GCP"; |
| 220 | import * as Effect from "effect/Effect"; |
| 221 | import Api from "./src/Api.ts"; |
| 222 | import Fulfillment from "./src/Fulfillment.ts"; |
| 223 | |
| 224 | export 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 | |
| 235 | Storage, 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 |
| 238 | Google-signed ID token, and `GCP.AIPlatform.GenerateContent` calls |
| 239 | Gemini. Besides Pub/Sub, event sources cover Storage, Cloud Scheduler, |
| 240 | and Eventarc. |
| 241 | |
| 242 | The default region comes from your profile (else `us-central1`), and |
| 243 | `GCP.Region("europe-west1")` overrides it for a stack. Reconciles wait |
| 244 | until 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 |
| 248 | test. |
| 249 | |
| 250 | Docs: [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 |
| 259 | Node 24, and bindings wire in Postgres and object storage without |
| 260 | passing your deployment API key to the application: |
| 261 | |
| 262 | ```typescript |
| 263 | // src/resources.ts |
| 264 | export 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 |
| 276 | export 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 | |
| 299 | Thirteen frameworks deploy as Fetch Functions, with the same props as |
| 300 | on 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 | |
| 319 | Also 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 |
| 329 | Fetch runtime for Functions; `.pipe(Alchemy.remote())` deploys one for |
| 330 | real. |
| 331 | |
| 332 | Docs: [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 | |
| 338 | A 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 |
| 342 | resource provider, or captured by a Worker or Lambda — resolves from the |
| 343 | list you give `Alchemy.Stack`'s new `secrets` option. Nothing is written |
| 344 | to `process.env` or to disk. |
| 345 | |
| 346 | ```typescript |
| 347 | // alchemy.run.ts |
| 348 | import * as Alchemy from "alchemy"; |
| 349 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 350 | import * as Doppler from "alchemy/Doppler"; |
| 351 | import * as Effect from "effect/Effect"; |
| 352 | import Api from "./src/api.ts"; |
| 353 | |
| 354 | export 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 | |
| 374 | The Worker reads the secret the same way it always has: |
| 375 | |
| 376 | ```typescript |
| 377 | // src/api.ts |
| 378 | export 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 | |
| 393 | Rotating the value in Doppler and running `alchemy deploy` now ships it: |
| 394 | a 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 | |
| 399 | Later entries override earlier ones. A missing key falls through to the |
| 400 | next source, and an empty string counts as a value. The process |
| 401 | environment is always the implicit last entry, so a one-off |
| 402 | `DATABASE_URL=… alchemy deploy` or a CI runner's exported variables |
| 403 | still win. List `Alchemy.Secrets.ProcessEnv()` yourself to rank it |
| 404 | elsewhere, 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 |
| 409 | receives the stage, so one stack serves `dev`, `pr-42`, and `prod` from |
| 410 | different places: |
| 411 | |
| 412 | ```typescript |
| 413 | secrets: ({ 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 | |
| 421 | On your machine, `alchemy profile edit --add Doppler` opens a browser |
| 422 | login, and `alchemy profile edit --add Infisical` stores a Universal |
| 423 | Auth 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 |
| 426 | the platform's identity token on GitHub Actions, GitLab CI, Vercel, or |
| 427 | Google Cloud for a short-lived one on every run: |
| 428 | |
| 429 | ```yaml |
| 430 | jobs: |
| 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, |
| 442 | never their values. Any Effect Layer that provides a `ConfigProvider` |
| 443 | works as a custom entry. |
| 444 | |
| 445 | Docs: [Secret providers](/environments/secret-providers) · |
| 446 | [Doppler](/environments/doppler) · [Infisical](/environments/infisical). |
| 447 | |
| 448 | ## Prisma ORM v8 |
| 449 | |
| 450 | Prisma ORM v8 runs on an Effect-native Postgres runtime |
| 451 | ([#1198](https://github.com/alchemy-run/alchemy/pull/1198)). Queries, |
| 452 | prepared statements, streams, and transactions are Effects with typed |
| 453 | errors and per-execution connection cleanup. Write the contract in |
| 454 | TypeScript or in Prisma Schema Language (PSL); both use the same |
| 455 | runtime. Prisma stays an optional peer. |
| 456 | |
| 457 | ### TypeScript-first |
| 458 | |
| 459 | Define the contract with Prisma's own builders: |
| 460 | |
| 461 | ```typescript |
| 462 | // src/prisma/contract.ts |
| 463 | import { defineContract } from "alchemy/Prisma/ORM"; |
| 464 | |
| 465 | export 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 | |
| 487 | Pass it straight to `Postgres` — no generated code: |
| 488 | |
| 489 | ```typescript |
| 490 | // src/api.ts |
| 491 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 492 | import * as PrismaPostgres from "alchemy/Prisma/ORM/Postgres"; |
| 493 | import * as Effect from "effect/Effect"; |
| 494 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 495 | import { Hyperdrive } from "./db.ts"; |
| 496 | import { contract } from "./prisma/contract.ts"; |
| 497 | |
| 498 | export 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 | |
| 517 | Keep `contract.psl` authoritative: |
| 518 | |
| 519 | ```prisma |
| 520 | // src/prisma/contract.psl |
| 521 | model User { |
| 522 | id id.uuidv7Native() |
| 523 | email String @unique |
| 524 | name String |
| 525 | posts Post[] |
| 526 | } |
| 527 | |
| 528 | model Post { |
| 529 | id id.uuidv7Native() |
| 530 | title String |
| 531 | authorId Uuid |
| 532 | author User @relation(fields: [authorId], references: [id]) |
| 533 | } |
| 534 | ``` |
| 535 | |
| 536 | Register Alchemy's Effect generator in `prisma.config.ts`, then run |
| 537 | `alchemy prisma generate` (or `--watch`): |
| 538 | |
| 539 | ```typescript |
| 540 | // prisma.config.ts |
| 541 | import { defineConfig as ormConfig } from "@prisma/orm-postgres/config"; |
| 542 | import { definePrismaConfig } from "prisma/config"; |
| 543 | import { withEffect } from "alchemy/Prisma/ORM/generator"; |
| 544 | |
| 545 | export 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 | |
| 556 | The Worker imports the generated `makeDatabase` instead of the |
| 557 | contract; 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 | |
| 570 | Both 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 |
| 573 | each form end to end on Neon through Hyperdrive, with migrations |
| 574 | applied on deploy by `Prisma.Contract` and `Prisma.Migrate`. |
| 575 | |
| 576 | Docs: [Contracts](/sql/prisma/contracts) · |
| 577 | [Postgres](/sql/prisma/postgres) · |
| 578 | [Migrations](/sql/prisma/migrations). |
| 579 | |
| 580 | ## Fly: a Service owns its App |
| 581 | |
| 582 | A `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 |
| 584 | longer 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 |
| 601 | methods beside `fetch`, and another Service binds it with |
| 602 | `Fly.bindService` for a typed client: |
| 603 | |
| 604 | ```typescript |
| 605 | // src/users.ts |
| 606 | export 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 |
| 615 | export 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 | |
| 629 | Calls are plain HTTP over Flycast. Each one carries the caller's token |
| 630 | and Fly's signed `Fly-Src` header, so only Services that bind `Users` |
| 631 | can call it. `network: yield* Fly.stackNetwork` isolates Services on a |
| 632 | private 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 | |
| 637 | Add `deploy: { strategy: "bluegreen" }` and the next `alchemy deploy` |
| 638 | starts 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 | |
| 652 | Websites take the same two props. An interrupted rollout resumes on the |
| 653 | next deploy, and a failed readiness check leaves the old version |
| 654 | serving. Rolling updates stay the default and remain the only option for |
| 655 | volume-backed Services. |
| 656 | |
| 657 | ### Multi-container Machines |
| 658 | |
| 659 | Run an API and its sidecar in one Machine with Fly's native container |
| 660 | groups, 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 |
| 666 | export const App = Fly.App("Preview"); |
| 667 | |
| 668 | export 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 | |
| 685 | Florian also added Flycast `private_v6` addresses to |
| 686 | `Fly.IpAssignment` |
| 687 | ([#1749](https://github.com/alchemy-run/alchemy/pull/1749)) and made |
| 688 | rolling updates wait for service checks before touching the next |
| 689 | replica ([#1670](https://github.com/alchemy-run/alchemy/pull/1670)). |
| 690 | Fly 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 | |
| 694 | Docs: [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 |
| 703 | takes an FQN, a logical ID, or a glob, and can repeat: |
| 704 | |
| 705 | ```sh |
| 706 | alchemy deploy --stage pr-123 --include 'Database/**' |
| 707 | alchemy dev --exclude 'Services/Background/**' |
| 708 | alchemy plan --include 'Database/Branch' --include 'Database/Password' |
| 709 | ``` |
| 710 | |
| 711 | Dependencies of included resources come along automatically; excluding |
| 712 | one fails the plan with its dependency chain. Excluded state is left |
| 713 | untouched. `Test.make`'s `deploy` takes the same filters: |
| 714 | |
| 715 | ```typescript |
| 716 | const { test, deploy } = Test.make({ providers: Cloudflare.providers() }); |
| 717 | |
| 718 | test( |
| 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 |
| 730 | Objects are the first host: jobs live in SQLite and fire from native |
| 731 | alarms, and scheduling joins the storage transaction, so the write and |
| 732 | the job commit together: |
| 733 | |
| 734 | ```typescript |
| 735 | export 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 | |
| 765 | Delivery is at least once with retries. Existing `scheduleEvent` jobs |
| 766 | keep running beside callbacks on the object's single alarm. Alarm |
| 767 | updates are now batched once per transaction |
| 768 | ([#1701](https://github.com/alchemy-run/alchemy/pull/1701)). |
| 769 | |
| 770 | Docs: [Durable Objects → Register a durable callback](/cloudflare/compute/durable-objects#register-a-durable-callback). |
| 771 | |
| 772 | ## Hibernating WebSocket RPC |
| 773 | |
| 774 | An `RpcDurableObject` that returns its handler Layer now serves Effect |
| 775 | RPC over hibernating WebSockets as well as HTTP |
| 776 | ([#1705](https://github.com/alchemy-run/alchemy/pull/1705)). The |
| 777 | request picks the transport; your code doesn't change. |
| 778 | |
| 779 | Define the RPCs once and share them between server and client: |
| 780 | |
| 781 | ```typescript |
| 782 | // rpcs.ts |
| 783 | import * as Schema from "effect/Schema"; |
| 784 | import { Rpc, RpcGroup } from "effect/rpc"; |
| 785 | |
| 786 | export class CounterRpcs extends RpcGroup.make( |
| 787 | Rpc.make("increment", { payload: {}, success: Schema.Number }), |
| 788 | ) {} |
| 789 | ``` |
| 790 | |
| 791 | Implement them in an `RpcDurableObject`: |
| 792 | |
| 793 | ```typescript |
| 794 | // counter.ts |
| 795 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 796 | import * as Effect from "effect/Effect"; |
| 797 | import { CounterRpcs } from "./rpcs.ts"; |
| 798 | |
| 799 | export 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 | |
| 819 | The Worker picks an object by name and forwards the connection to it: |
| 820 | |
| 821 | ```typescript |
| 822 | // worker.ts |
| 823 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 824 | import * as Effect from "effect/Effect"; |
| 825 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 826 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 827 | import Counter from "./counter.ts"; |
| 828 | |
| 829 | export 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 | |
| 847 | In the browser, `alchemy/Cloudflare/RpcWebSocketClient` builds a typed |
| 848 | client over one WebSocket. It imports only Effect modules, so it is safe |
| 849 | to bundle: |
| 850 | |
| 851 | ```typescript |
| 852 | // client.ts |
| 853 | import * as RpcWebSocketClient from "alchemy/Cloudflare/RpcWebSocketClient"; |
| 854 | import * as Context from "effect/Context"; |
| 855 | import * as Effect from "effect/Effect"; |
| 856 | import * as Layer from "effect/Layer"; |
| 857 | import * as RpcClient from "effect/rpc/RpcClient"; |
| 858 | import type { RpcClientError } from "effect/rpc/RpcClientError"; |
| 859 | import * as Socket from "effect/socket/Socket"; |
| 860 | import { CounterRpcs } from "./rpcs.ts"; |
| 861 | |
| 862 | class CounterClient extends Context.Service< |
| 863 | CounterClient, |
| 864 | RpcClient.FromGroup<typeof CounterRpcs, RpcClientError> |
| 865 | >()("CounterClient") {} |
| 866 | |
| 867 | const CounterClientLive = RpcWebSocketClient.layer( |
| 868 | CounterClient, |
| 869 | CounterRpcs, |
| 870 | "wss://example.com/counters/alice", |
| 871 | ).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)); |
| 872 | |
| 873 | const program = Effect.gen(function* () { |
| 874 | const counter = yield* CounterClient; |
| 875 | return yield* counter.increment({}); |
| 876 | }).pipe(Effect.provide(CounterClientLive)); |
| 877 | ``` |
| 878 | |
| 879 | Every call on that socket goes to the `"alice"` object. While the |
| 880 | connection is idle the Durable Object can hibernate; the next call wakes |
| 881 | it and is answered on the same socket. Alchemy handles the heartbeats and |
| 882 | restores the connection after a wake. In-flight calls are not replayed: |
| 883 | if the object restarts mid-call, the socket closes with code `1012`. |
| 884 | |
| 885 | Docs: [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 | |
| 890 | If you write your own WebSocket handlers on a plain `DurableObject` |
| 891 | instead of using RPC, you can now validate the data you attach to each |
| 892 | socket with an Effect Schema |
| 893 | ([#1723](https://github.com/alchemy-run/alchemy/pull/1723)). A session |
| 894 | read back after hibernation comes back decoded and typed, where |
| 895 | `deserializeAttachment` used to return an unchecked value: |
| 896 | |
| 897 | ```typescript |
| 898 | // room.ts |
| 899 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 900 | import * as Effect from "effect/Effect"; |
| 901 | import * as Schema from "effect/Schema"; |
| 902 | |
| 903 | const Session = Schema.Struct({ |
| 904 | id: Schema.String, |
| 905 | joinedAt: Schema.DateFromString, |
| 906 | }); |
| 907 | |
| 908 | export 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 | |
| 929 | A missing or invalid attachment fails with a typed |
| 930 | `WebSocketAttachmentError`, so you decide whether to close the socket or |
| 931 | ignore it. |
| 932 | |
| 933 | Docs: [Hibernatable WebSockets → Validate attachments after hibernation](/cloudflare/compute/hibernatable-websockets#validate-attachments-after-hibernation). |
| 934 | |
| 935 | ## Presigned R2 URLs in `alchemy dev` |
| 936 | |
| 937 | Presigned R2 URLs and S3 clients work under `alchemy dev` with no extra |
| 938 | configuration ([#1793](https://github.com/alchemy-run/alchemy/pull/1793)). |
| 939 | The same code signs against the Worker's local dev URL in dev and |
| 940 | against `r2.cloudflarestorage.com` when deployed: |
| 941 | |
| 942 | ```typescript |
| 943 | export const Uploads = Cloudflare.R2.Bucket("Uploads"); |
| 944 | |
| 945 | export 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 | |
| 960 | Async Workers get the same thing through |
| 961 | `Cloudflare.R2.S3Credentials(bucket, { access: "write" })`, which |
| 962 | delivers an endpoint and scoped keys for any S3 client. The local S3 |
| 963 | endpoint runs inside the Worker's existing workerd process, and the dev |
| 964 | proxy now answers `Expect: 100-continue`, which took a 3 MB AWS SDK |
| 965 | `PutObject` from ~6 s to ~130 ms. |
| 966 | |
| 967 | Docs: [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 |
| 972 | its `source` ([#1722](https://github.com/alchemy-run/alchemy/pull/1722), |
| 973 | [#1505](https://github.com/alchemy-run/alchemy/pull/1505)) — including |
| 974 | one owned by another stack: |
| 975 | |
| 976 | ```typescript |
| 977 | export 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 | |
| 991 | Sources include Images, KV, R2, Vectorize, Workers, Workflows, and two |
| 992 | new 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 |
| 994 | bucket. Thanks [Alex](https://github.com/agcty) for the Workflow |
| 995 | sources and `Cloudflare.Workflow.ref`! |
| 996 | |
| 997 | Docs: [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 |
| 1002 | ACME 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 |
| 1007 | export const Site = Fly.App("Site"); |
| 1008 | |
| 1009 | export 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 |
| 1032 | runtime issuance, and `Fly.WriteCertificates` manages an App's |
| 1033 | certificates at runtime. |
| 1034 | |
| 1035 | Docs: [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 |
| 1040 | registry, and any connection can now name a registry, so workloads |
| 1041 | built from `main` or a Docker context deploy to GKE, AKS, k3s, or your |
| 1042 | laptop ([#1779](https://github.com/alchemy-run/alchemy/pull/1779)): |
| 1043 | |
| 1044 | ```typescript |
| 1045 | export 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 | |
| 1059 | Editing a `Job` or `Deployment`'s `main` program now redeploys it. A |
| 1060 | new `/kubernetes` docs hub has a five-part tutorial that starts on |
| 1061 | `LocalCluster` and ends on your own cluster. |
| 1062 | |
| 1063 | Docs: [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 |
| 1070 | follows the task's retry policy; once retries run out, `catchTag` sees |
| 1071 | your error, including after Cloudflare replays the step: |
| 1072 | |
| 1073 | ```typescript |
| 1074 | class CardDeclined extends Data.TaggedError("CardDeclined")<{ |
| 1075 | reason: string; |
| 1076 | }> {} |
| 1077 | |
| 1078 | const chargeCard = (orderId: string) => |
| 1079 | Effect.fail(new CardDeclined({ reason: `insufficient funds for ${orderId}` })); |
| 1080 | |
| 1081 | export 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 |
| 1098 | an explicit account-global `workflowName` |
| 1099 | ([#1460](https://github.com/alchemy-run/alchemy/pull/1460)) — thanks |
| 1100 | [Odysseas Papadimas](https://github.com/odysseaspapadimas)! A |
| 1101 | retries-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 |
| 1104 | own scope ([#1701](https://github.com/alchemy-run/alchemy/pull/1701)). |
| 1105 | |
| 1106 | Docs: [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) |
| 1111 | apps 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 |
| 1117 | export 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 | |
| 1130 | On Cloudflare it deploys a Worker with KV caching and prerender seeding, |
| 1131 | without Wrangler or OpenNext. AWS runs it on streaming Lambda with an S3 |
| 1132 | cache behind CloudFront. Fly, Hetzner, Railway, and Prisma run vinext's |
| 1133 | Node 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 | |
| 1155 | Docs: [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 | |
| 1161 | Next.js on Cloudflare no longer needs `open-next.config.ts`, and |
| 1162 | writable 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 | |
| 1181 | Octane apps no longer need a hosting adapter in `octane.config.ts` on |
| 1182 | any 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 |
| 1185 | moves to `3.0.0-next.27`, and the Node target now ships client assets |
| 1186 | and 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 |
| 1189 | and Neon servers start from the built config, so a read-only deploy no |
| 1190 | longer tries to download SWC |
| 1191 | ([#1805](https://github.com/alchemy-run/alchemy/pull/1805)) — thanks |
| 1192 | [Aman Varshney](https://github.com/AmanVarshney01)! |
| 1193 | |
| 1194 | Docs: [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) |