From 2d7bb42efb61f2d379deaf33f2afd59381434415 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 15 Jul 2026 14:15:31 -0700 Subject: [PATCH] docs(blog): backfill release blogs for 0.62 through 0.93 Adds 25 release blog posts covering every minor release line since the last published blog post (Aug 2025). Each post highlights the headline features with code examples, folds the rest into an "Also in this release" list, and thanks the contributors from the changelog. Bugfix-only minors (0.73, 0.75, 0.76, 0.78, 0.79, 0.84, 0.89) are intentionally skipped. Co-Authored-By: Claude Fable 5 --- .../docs/blog/2025-08-15-alchemy-0-62.md | 91 +++++++++++++ .../docs/blog/2025-08-25-alchemy-0-63.md | 100 +++++++++++++++ .../docs/blog/2025-08-27-alchemy-0-64.md | 47 +++++++ .../docs/blog/2025-09-06-alchemy-0-65.md | 104 +++++++++++++++ .../docs/blog/2025-09-12-alchemy-0-66.md | 82 ++++++++++++ .../docs/blog/2025-09-16-alchemy-0-67.md | 81 ++++++++++++ .../docs/blog/2025-09-24-alchemy-0-68.md | 93 ++++++++++++++ .../docs/blog/2025-09-28-alchemy-0-69.md | 81 ++++++++++++ .../docs/blog/2025-10-01-alchemy-0-70.md | 97 ++++++++++++++ .../docs/blog/2025-10-08-alchemy-0-71.md | 113 +++++++++++++++++ .../docs/blog/2025-10-09-alchemy-0-72.md | 48 +++++++ .../docs/blog/2025-10-15-alchemy-0-74.md | 84 ++++++++++++ .../docs/blog/2025-10-31-alchemy-0-77.md | 83 ++++++++++++ .../docs/blog/2025-12-04-alchemy-0-80.md | 97 ++++++++++++++ .../docs/blog/2025-12-10-alchemy-0-81.md | 89 +++++++++++++ .../docs/blog/2025-12-19-alchemy-0-82.md | 54 ++++++++ .../docs/blog/2026-01-04-alchemy-0-83.md | 96 ++++++++++++++ .../docs/blog/2026-02-18-alchemy-0-85.md | 116 +++++++++++++++++ .../docs/blog/2026-02-24-alchemy-0-86.md | 45 +++++++ .../docs/blog/2026-02-25-alchemy-0-87.md | 40 ++++++ .../docs/blog/2026-03-11-alchemy-0-88.md | 97 ++++++++++++++ .../docs/blog/2026-03-20-alchemy-0-90.md | 50 ++++++++ .../docs/blog/2026-04-09-alchemy-0-91.md | 94 ++++++++++++++ .../docs/blog/2026-04-20-alchemy-0-92.md | 81 ++++++++++++ .../docs/blog/2026-04-24-alchemy-0-93.md | 120 ++++++++++++++++++ 25 files changed, 2083 insertions(+) create mode 100644 alchemy-web/src/content/docs/blog/2025-08-15-alchemy-0-62.md create mode 100644 alchemy-web/src/content/docs/blog/2025-08-25-alchemy-0-63.md create mode 100644 alchemy-web/src/content/docs/blog/2025-08-27-alchemy-0-64.md create mode 100644 alchemy-web/src/content/docs/blog/2025-09-06-alchemy-0-65.md create mode 100644 alchemy-web/src/content/docs/blog/2025-09-12-alchemy-0-66.md create mode 100644 alchemy-web/src/content/docs/blog/2025-09-16-alchemy-0-67.md create mode 100644 alchemy-web/src/content/docs/blog/2025-09-24-alchemy-0-68.md create mode 100644 alchemy-web/src/content/docs/blog/2025-09-28-alchemy-0-69.md create mode 100644 alchemy-web/src/content/docs/blog/2025-10-01-alchemy-0-70.md create mode 100644 alchemy-web/src/content/docs/blog/2025-10-08-alchemy-0-71.md create mode 100644 alchemy-web/src/content/docs/blog/2025-10-09-alchemy-0-72.md create mode 100644 alchemy-web/src/content/docs/blog/2025-10-15-alchemy-0-74.md create mode 100644 alchemy-web/src/content/docs/blog/2025-10-31-alchemy-0-77.md create mode 100644 alchemy-web/src/content/docs/blog/2025-12-04-alchemy-0-80.md create mode 100644 alchemy-web/src/content/docs/blog/2025-12-10-alchemy-0-81.md create mode 100644 alchemy-web/src/content/docs/blog/2025-12-19-alchemy-0-82.md create mode 100644 alchemy-web/src/content/docs/blog/2026-01-04-alchemy-0-83.md create mode 100644 alchemy-web/src/content/docs/blog/2026-02-18-alchemy-0-85.md create mode 100644 alchemy-web/src/content/docs/blog/2026-02-24-alchemy-0-86.md create mode 100644 alchemy-web/src/content/docs/blog/2026-02-25-alchemy-0-87.md create mode 100644 alchemy-web/src/content/docs/blog/2026-03-11-alchemy-0-88.md create mode 100644 alchemy-web/src/content/docs/blog/2026-03-20-alchemy-0-90.md create mode 100644 alchemy-web/src/content/docs/blog/2026-04-09-alchemy-0-91.md create mode 100644 alchemy-web/src/content/docs/blog/2026-04-20-alchemy-0-92.md create mode 100644 alchemy-web/src/content/docs/blog/2026-04-24-alchemy-0-93.md diff --git a/alchemy-web/src/content/docs/blog/2025-08-15-alchemy-0-62.md b/alchemy-web/src/content/docs/blog/2025-08-15-alchemy-0-62.md new file mode 100644 index 0000000000..db3116b921 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-08-15-alchemy-0-62.md @@ -0,0 +1,91 @@ +--- +title: "Alchemy 0.62: Scoped AWS Credentials" +date: 2025-08-15 +authors: Sam Goodwin +excerpt: Deploy AWS resources across multiple accounts and regions from a single alchemy.run.ts with global, scope, and resource-level credential overrides. Plus, alchemy create now keeps secrets out of git and configures CloudflareStateStore for CI. +--- + +Alchemy 0.62 lets you deploy AWS resources across multiple accounts and regions in a single deployment. Credentials can now be overridden at three levels — global environment variables, an entire scope, or an individual resource — so a multi-account architecture no longer needs multiple state files or wrapper scripts. + +The `alchemy create` CLI also got smarter about the boring-but-important stuff: it appends `.alchemy` to your `.gitignore` so local state and secrets never get committed, and it configures `CloudflareStateStore` automatically when you choose GitHub Actions for CI. + + + +## Multi-account AWS deployments + +AWS credentials now resolve through a three-tier hierarchy: resource-level overrides win over scope-level credentials, which win over the global environment (`AWS_PROFILE`, `AWS_REGION`, etc.). + +Every AWS EC2 networking resource accepts credential properties directly, so two VPCs in two different accounts is just two resources: + +```ts +import alchemy from "alchemy"; +import { Vpc } from "alchemy/aws/ec2"; + +const app = await alchemy("multi-account"); + +// uses AWS_PROFILE / AWS_REGION from the environment +const mainVpc = await Vpc("main-vpc", { + cidrBlock: "10.0.0.0/16", +}); + +// deployed to a different account and region +const secondaryVpc = await Vpc("secondary-vpc", { + cidrBlock: "10.1.0.0/16", + profile: "secondary-account", + region: "us-east-1", +}); + +await app.finalize(); +``` + +To apply credentials to a whole group of resources, set them on a scope with the `aws` option: + +```ts +await alchemy.run( + "production", + { + aws: { + region: "us-east-1", + profile: "production-account", + }, + }, + async () => { + // everything in here uses the production-account profile + await Vpc("prod-vpc", { + cidrBlock: "10.0.0.0/16", + }); + }, +); +``` + +Cross-account role assumption works too — hand a resource a `roleArn` and Alchemy assumes it before making API calls: + +```ts +const vpc = await Vpc("cross-account-vpc", { + cidrBlock: "10.0.0.0/16", + roleArn: "arn:aws:iam::123456789012:role/DeploymentRole", + roleSessionName: "alchemy-deployment", + region: "us-west-2", +}); +``` + +Static credentials (`accessKeyId`, `secretAccessKey`, `sessionToken`) are typed as `Secret`, so if you do pass them explicitly, they're encrypted in your state file like any other Alchemy secret. + +See the [AWS credential overrides docs](/providers/aws/credential-overrides) for the full property list and resolution rules. + +## A more careful `alchemy create` + +Two quality-of-life improvements landed in the CLI this release: + +- **`.alchemy` is appended to `.gitignore`** during project init (and `.wrangler/` too, as of 0.62.3). Local state and dev secrets stay out of your repo without you having to remember. +- **Choosing GitHub Actions configures `CloudflareStateStore`.** CI needs shared state, so if you pick GitHub Actions in the create flow, your template is generated with `CloudflareStateStore` instead of the local filesystem store, the workflow sets `CLOUDFLARE_EMAIL`, `ALCHEMY_PASSWORD`, and `ALCHEMY_STATE_TOKEN`, and the workflow commands match whichever package manager you chose. + +## Also in this release + +- **Monorepo support.** Multiple instances of Alchemy (e.g. different package versions across workspaces) can now coexist in a monorepo without clobbering each other. +- **Template names are stage-aware.** Generated templates substitute the project name to construct stage-specific resource names, and no longer modify your app's stage. +- **Fixes.** Worker asset and no-bundle file paths now resolve correctly on Windows; large R2 buckets can be deleted; `CloudflareStateStore` retries transient errors; D1 phases use the correct `databaseId`; `spa: true` can be overridden in Vite apps; and `alchemy dev` spawns Vite idempotently and cleans up its processes on exit. + +## Thanks + +Thanks to **yehudacohen**, **Aman Varshney**, **Michael K**, **John Royal**, and **Rhayxz** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.61.0...v0.62.3) for everything in the 0.62 line. diff --git a/alchemy-web/src/content/docs/blog/2025-08-25-alchemy-0-63.md b/alchemy-web/src/content/docs/blog/2025-08-25-alchemy-0-63.md new file mode 100644 index 0000000000..3f966b30f4 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-08-25-alchemy-0-63.md @@ -0,0 +1,100 @@ +--- +title: "Alchemy 0.63: Automatic Physical Names" +date: 2025-08-25 +authors: Sam Goodwin +excerpt: Resources without an explicit name now get one generated from your app, scope chain, and stage — so the same alchemy.run.ts deploys cleanly to any stage without collisions. Plus a --adopt CLI flag and R2 bucket lifecycle and lock rules. +--- + +Alchemy 0.63 changes how resources are named. If you don't give a resource an explicit `name`, Alchemy now generates one from your app name, scope chain, resource ID, and stage — which means the same `alchemy.run.ts` deploys to `dev`, `prod`, and a preview stage per PR without a single name collision. + +This release also adds a `--adopt` flag to the CLI so you can adopt existing resources without sprinkling `adopt: true` everywhere, and R2 buckets picked up lifecycle and lock rules. + + + +## Physical names from app, stage, and resource ID + +Previously, omitting `name` meant the resource ID was used directly as the physical name. That works until you deploy a second stage and Cloudflare tells you a Worker called `worker` already exists. + +Now the generated name encodes where the resource lives: + +```ts +import alchemy from "alchemy"; +import { Worker } from "alchemy/cloudflare"; + +const app = await alchemy("my-app"); + +const worker = await Worker("worker"); + +console.log(worker.name); // "my-app-worker-${stage}" +``` + +Run `alchemy deploy --stage prod` and the Worker is named `my-app-worker-prod`. Nested scopes join the chain too: + +```ts +await alchemy.run("backend", async () => { + await Worker("worker"); // my-app-backend-worker-prod +}); +``` + +This applies across providers — Cloudflare, AWS, PlanetScale, Neon, Docker, Vercel, Upstash, Sentry, and more all default to `this.scope.createPhysicalName(id)` when no name is given. + +Existing resources are unaffected: names resolve as `props.name ?? this.output?.name ?? generated`, so anything already deployed keeps the name it has. Only newly created resources pick up the new scheme. + +See the [resource docs](/concepts/resource) for more on physical names. + +## Blanket adoption with `--adopt` + +If a resource already exists in your account (created by hand, or by a previous state file), Alchemy normally refuses to take it over unless you set `adopt: true` on the resource. That's the right default, but tedious when migrating a whole app. + +The CLI now takes a flag that applies to every resource in the run: + +```sh +alchemy deploy --adopt +alchemy dev --adopt +``` + +## R2 lifecycle and lock rules + +`R2Bucket` now supports Cloudflare's object lifecycle and retention lock policies as plain props: + +```ts +import { R2Bucket } from "alchemy/cloudflare"; + +const bucket = await R2Bucket("logs", { + lifecycle: [ + // abort incomplete multipart uploads after 7 days + { + id: "abort-mpu-7d", + abortMultipartUploadsTransition: { + condition: { type: "Age", maxAge: 7 * 24 * 60 * 60 }, + }, + }, + // delete archived objects after 30 days + { + id: "delete-30d", + conditions: { prefix: "archive/" }, + deleteObjectsTransition: { + condition: { type: "Age", maxAge: 30 * 24 * 60 * 60 }, + }, + }, + ], + lock: [ + // prevent deletion or overwrite for 90 days + { + id: "retain-90d", + condition: { type: "Age", maxAgeSeconds: 90 * 24 * 60 * 60 }, + }, + ], +}); +``` + +Rules support age and date conditions, prefix scoping, and storage-class transitions to Infrequent Access. See the [R2 Bucket docs](/providers/cloudflare/bucket) for the full set. + +## Also in this release + +- **Hono template.** `alchemy create` now offers a Hono starter template. +- **Fixes.** React Router apps can be deployed in SPA mode; Astro output type checks no longer trip on path validation and Astro correctly sets `spa: false`; a `WranglerJson` type-instantiation error is resolved; and `find-process` imports cleanly on plain Node. + +## Thanks + +Thanks to **Aman Varshney** and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.62.3...v0.63.1) for everything in the 0.63 line. diff --git a/alchemy-web/src/content/docs/blog/2025-08-27-alchemy-0-64.md b/alchemy-web/src/content/docs/blog/2025-08-27-alchemy-0-64.md new file mode 100644 index 0000000000..992d1188e8 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-08-27-alchemy-0-64.md @@ -0,0 +1,47 @@ +--- +title: "Alchemy 0.64: Flatter Website Scopes" +date: 2025-08-27 +authors: Sam Goodwin +excerpt: Website no longer hides its Worker inside a nested scope, so moving between Website and a plain Worker is seamless. Plus R2 jurisdiction is now respected in bindings and wrangler.json. +--- + +Alchemy 0.64 flattens the `Website` scope hierarchy. `Website` (and the framework resources built on it — `Vite`, `Astro`, `Nuxt`, `ReactRouter`, and friends) used to create its Worker inside a nested scope via `alchemy.run`, which made it painful to refactor between a `Website` and a plain `Worker`: the resource lived at a different address in your state, so Alchemy would try to delete one and create the other. + + + +## One less layer of scope + +The Worker created by a `Website` now lives directly in your scope, and the `WranglerJson` helper is a plain function instead of a Resource. In state terms: + +``` +before: my-app/prod/website/worker +after: my-app/prod/website +``` + +The upshot is that this refactor is now a non-event: + +```ts +// before: a full-stack site +const site = await Vite("website", { + entrypoint: "./src/worker.ts", +}); + +// after: you outgrew the wrapper — same state, same Worker +const worker = await Worker("website", { + entrypoint: "./src/worker.ts", +}); +``` + +The migration is handled automatically. When Alchemy encounters a Worker that used to live under a `Website` scope, it adopts the existing Worker on create and skips the delete of the old nested entry — so upgrading to 0.64 replaces nothing. + +## Also in this release + +- **R2 jurisdiction respected everywhere.** A bucket's `jurisdiction` (e.g. `eu`) is now drilled through to Worker bindings and the generated `wrangler.json`, and buckets without one send `undefined` instead of `"default"`. +- **Node compat synced with workers-sdk.** The `nodejs_compat` plugin is aligned with the latest Cloudflare workers-sdk behavior. +- **`cloudflare:*` modules marked external.** Imports like `cloudflare:workers` are no longer touched by the bundler. +- **Templates use generated names.** Starter templates no longer hard-code physical names — they lean on the physical name generation introduced in 0.63. +- **`@cloudflare/workers-types` peer dependency upgraded.** + +## Thanks + +Thanks to **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.63.1...v0.64.0) for everything in 0.64. diff --git a/alchemy-web/src/content/docs/blog/2025-09-06-alchemy-0-65.md b/alchemy-web/src/content/docs/blog/2025-09-06-alchemy-0-65.md new file mode 100644 index 0000000000..ae7bcf66c7 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-09-06-alchemy-0-65.md @@ -0,0 +1,104 @@ +--- +title: "Alchemy 0.65: Debugging, Next.js, and PlanetScale Postgres" +date: 2025-09-06 +authors: Sam Goodwin +excerpt: Attach a debugger to your alchemy.run.ts and your Workers with the new --inspect flags, deploy Next.js to Cloudflare with a single resource, and provision PlanetScale Postgres databases with scoped roles. +--- + +Alchemy 0.65 is a big one. You can now attach a real debugger — breakpoints, step-through, the works — to both your `alchemy.run.ts` and the Workers it deploys, straight from the CLI. There's a new `Nextjs` resource that deploys Next.js apps to Cloudflare via OpenNext, and PlanetScale support now covers their Postgres offering with a new `Role` resource for scoped credentials. + + + +## Debug via the CLI + +All Alchemy runtime commands now accept `--inspect`, `--inspect-wait`, and `--inspect-brk`: + +```sh +alchemy dev --inspect-wait +alchemy deploy --inspect +alchemy destroy --inspect +``` + +Under the hood, the CLI runs a Chrome DevTools Protocol management server that aggregates every debuggable process in your session — the `alchemy.run.ts` program itself and each Cloudflare Worker it runs — behind a single endpoint. Messages are tracked and replayable, so a debugger that attaches late still sees the full picture. It works with VSCode, the Bun and Node web debuggers, and Chrome DevTools. + +`--inspect-wait` pauses until a debugger connects, and `--inspect-brk` breaks on the first line — handy when the bug is in your infrastructure code rather than your app. + +The [debugging guide](/guides/debugging) walks through the VSCode setup: one task, one launch config, and breakpoints work across your infra and runtime code. + +## Next.js on Cloudflare + +The new `Nextjs` resource deploys a Next.js app to Cloudflare Workers using [OpenNext](https://opennext.js.org/cloudflare): + +```ts +import alchemy from "alchemy"; +import { Nextjs } from "alchemy/cloudflare"; + +const app = await alchemy("my-nextjs-app"); + +export const website = await Nextjs("website"); + +console.log({ url: website.url }); + +await app.finalize(); +``` + +It takes the same props as `Website`, and defaults the whole OpenNext pipeline for you: `opennextjs-cloudflare build` as the build command, `next dev` for local dev, the `.open-next` entrypoint and assets, and the `nodejs_compat` compatibility flags. `alchemy create` also gained a Next.js template if you're starting fresh. + +Follow the [Next.js guide](/guides/cloudflare-nextjs) to get started. + +## PlanetScale Postgres + +The PlanetScale provider now supports Postgres databases alongside MySQL. Set `kind: "postgresql"` on a `Database` and use the new `Role` resource to mint scoped credentials: + +```ts +import alchemy from "alchemy"; +import { Database, Role } from "alchemy/planetscale"; + +const app = await alchemy("my-app"); + +const database = await Database("db", { + name: "my-app-db", + clusterSize: "PS_10", + kind: "postgresql", +}); + +const role = await Role("db-role", { + database, + inheritedRoles: ["postgres"], +}); + +await app.finalize(); +``` + +Roles support Postgres's built-in permission sets and an optional TTL for short-lived access: + +```ts +const reader = await Role("reader", { + database, + inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"], + ttl: 3600, // expires after an hour +}); +``` + +Every role exposes `connectionUrl` and `connectionUrlPooled` as Secrets — pass one to a Cloudflare [Hyperdrive](/providers/cloudflare/hyperdrive) and bind it to a Worker: + +```ts +import { Hyperdrive } from "alchemy/cloudflare"; + +const hyperdrive = await Hyperdrive("db-hyperdrive", { + origin: role.connectionUrl, +}); +``` + +See the [Database](/providers/planetscale/database) and [Role](/providers/planetscale/role) docs for details. + +## Also in this release + +- **Hyperdrive adoption.** `Hyperdrive` now supports `adopt: true` when the name conflicts with an existing configuration. +- **`alchemy.run.mts`.** The CLI now picks up `.mts` entrypoints. +- **CloudflareStateStore validates `stateToken`.** Missing tokens now fail fast with a clear error instead of confusing API failures later. +- **Fixes.** Worker asset uploads retry with better error handling and use the correct `text/javascript` MIME type; producer-only queue bindings render correctly in `wrangler.json`; `?module` suffixes in WASM imports are handled; and function memoization uses deterministic cache keys. + +## Thanks + +Thanks to **Michael K**, **John Royal**, **Matt 'TK' Taylor**, and **Rahul Mishra** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.64.0...v0.65.1) for everything in the 0.65 line. diff --git a/alchemy-web/src/content/docs/blog/2025-09-12-alchemy-0-66.md b/alchemy-web/src/content/docs/blog/2025-09-12-alchemy-0-66.md new file mode 100644 index 0000000000..d9f3384f1e --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-09-12-alchemy-0-66.md @@ -0,0 +1,82 @@ +--- +title: "Alchemy 0.66: Multi-App Monorepos" +date: 2025-09-12 +authors: Sam Goodwin +excerpt: Alchemy apps can now import each other inside a monorepo. Export a Worker from your backend's alchemy.run.ts, import it in your frontend, and bind to it directly — the new `--app` flag keeps every other app in safe read-only mode. +--- + +Alchemy 0.66 adds support for multi-app, interconnected monorepos. You can now split your infrastructure across multiple `alchemy.run.ts` files — one per app — and have them import each other like ordinary TypeScript modules. A frontend can bind directly to a Worker exported by the backend's Alchemy program, and Turborepo (or any task runner) orchestrates the deploy order. + +The key piece is the new `--app` flag on `alchemy deploy`, `dev`, and `destroy`. When you select an app with `--app frontend`, every *other* Alchemy program that gets imported runs in read mode: its outputs are resolved from state without deploying or mutating anything. + + + +## One repo, many apps + +The backend defines its infrastructure and exports the Worker: + +```ts +// apps/backend/alchemy.run.ts +import alchemy from "alchemy"; +import { D1Database, Worker } from "alchemy/cloudflare"; +import path from "node:path"; + +const app = await alchemy("backend"); + +const db = await D1Database("db"); + +export const backend = await Worker("worker", { + // resolve relative to this file so it works when imported from another app + entrypoint: path.join(import.meta.dirname, "src", "worker.ts"), + bindings: { + db, + }, +}); + +await app.finalize(); +``` + +The frontend imports the backend's program and binds to its Worker like any other resource: + +```ts +// apps/frontend/alchemy.run.ts +import alchemy from "alchemy"; +import { Vite } from "alchemy/cloudflare"; +import { backend } from "backend/alchemy"; + +const app = await alchemy("frontend"); + +export const frontend = await Vite("website", { + bindings: { + backend, + }, +}); + +await app.finalize(); +``` + +Each app passes `--app {name}` in its scripts so that only the selected app is mutated: + +```json +// apps/frontend/package.json +{ + "scripts": { + "dev": "alchemy dev --app frontend", + "deploy": "alchemy deploy --app frontend", + "destroy": "alchemy destroy --app frontend" + } +} +``` + +From there, Turborepo's `dependsOn` handles ordering — `backend` deploys before `frontend`, and destroys run in reverse. Running `bun dev` at the root gives you a TUI pane per app, each running its own local simulation. + +The full setup — turbo.json tasks, package exports, reverse-order destroy — is covered in the new [Turborepo guide](/guides/turborepo). + +## Also in this release + +- **Stripe SDK bump.** The `stripe` dependency was bumped to the latest API version. Stripe deprecated the singular `coupon` on customers, so the `coupon` property was removed from `Customer`, and the legacy `aggregateUsage` billing option was dropped from `Price` in favor of meters. Thanks **Michael K**. +- **Fixes.** The Next.js init script now generates a correct `./env.d.ts`. + +## Thanks + +Thanks to **Michael K** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.65.1...v0.66.0) for everything in 0.66. diff --git a/alchemy-web/src/content/docs/blog/2025-09-16-alchemy-0-67.md b/alchemy-web/src/content/docs/blog/2025-09-16-alchemy-0-67.md new file mode 100644 index 0000000000..93e1e8a131 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-09-16-alchemy-0-67.md @@ -0,0 +1,81 @@ +--- +title: "Alchemy 0.67: SecretRef for Existing Secrets" +date: 2025-09-16 +authors: Sam Goodwin +excerpt: Bind Workers to secrets that already exist in Cloudflare's Secrets Store with SecretRef — no need to hand the value to Alchemy. Plus HyperdriveRef for existing Hyperdrive configs and public dev tunnels via cloudflared. +--- + +Not every secret should live in your Alchemy program. Sometimes a value is already sitting in Cloudflare's Secrets Store — put there by another team, another tool, or another app — and you just want to bind to it. Alchemy 0.67 adds `SecretRef` for exactly that, alongside `HyperdriveRef` for existing Hyperdrive configs and a new `--tunnel` flag that exposes your local dev Worker to the public internet. + + + +## SecretRef: bind existing secrets + +`SecretRef` references a secret in a [Secrets Store](/providers/cloudflare/secrets-store) by name. It never creates or updates the value — that stays wherever it was set — but the binding works exactly like a [`Secret`](/providers/cloudflare/secret) you manage with Alchemy: + +```ts +import { SecretRef, Worker } from "alchemy/cloudflare"; + +const apiKeyRef = await SecretRef({ name: "API_KEY" }); + +const worker = await Worker("worker", { + entrypoint: "./src/worker.ts", + url: true, + bindings: { + API_KEY: apiKeyRef, + }, +}); +``` + +At runtime, it's the same Secrets Store binding: + +```ts +export default { + async fetch(request, env) { + const key = await env.API_KEY.get(); + return new Response(key ? "ok" : "missing"); + }, +}; +``` + +By default it resolves against Cloudflare's `default_secrets_store`; pass `store` to target a specific one. Since it manages no state, deleting a `SecretRef` from your app is a no-op in Cloudflare — the secret stays put. Thanks **Andrew Jefferson** for the contribution. + +`HyperdriveRef` follows the same pattern for [Hyperdrive](/providers/cloudflare/hyperdrive) configs that already exist — reference one by name (looked up via the API) or directly by ID: + +```ts +import { HyperdriveRef, Worker } from "alchemy/cloudflare"; + +const hyperdrive = await HyperdriveRef({ name: "my-hyperdrive" }); + +const worker = await Worker("api", { + entrypoint: "./src/index.ts", + bindings: { + DB: hyperdrive, + }, +}); +``` + +## Public tunnels for local dev + +`alchemy dev` can now route requests from the public internet to your locally running Worker through a Cloudflare Quick Tunnel. Enable it per Worker: + +```ts +const worker = await Worker("my-worker", { + entrypoint: "./src/worker.ts", + dev: { + tunnel: true, + }, +}); +``` + +Or for everything at once with `alchemy dev --tunnel`. Under the hood, a temporary Quick Tunnel is created and the deployed Worker proxies requests through it to your local process — so webhooks, OAuth callbacks, and anyone you send the URL to hit the code running on your machine. You'll need [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed. See the [dev docs](/concepts/dev) for details. + +## Also in this release + +- **Named entrypoints for service bindings.** Bind to a specific `WorkerEntrypoint` exported by another Worker with `Worker.experimentalEntrypoint(api, "AdminEntrypoint")`. +- **Next.js minification.** Next.js Worker bundles are now minified, reducing the likelihood of hitting Cloudflare size limits. +- **Fixes.** The CLI now ignores `turbo.json` when finding the workspace root; the unsupported AI Gateway binding type was removed; and commands run by `scope.spawn` properly mirror output to stdio. + +## Thanks + +Thanks to **Andrew Jefferson**, **John Royal**, and **Michael K** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.66.0...v0.67.0) for everything in 0.67. diff --git a/alchemy-web/src/content/docs/blog/2025-09-24-alchemy-0-68.md b/alchemy-web/src/content/docs/blog/2025-09-24-alchemy-0-68.md new file mode 100644 index 0000000000..9b784466dd --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-09-24-alchemy-0-68.md @@ -0,0 +1,93 @@ +--- +title: "Alchemy 0.68: Simpler Resources and CLI Auth for Cloudflare" +date: 2025-09-24 +authors: Sam Goodwin +excerpt: Custom resources no longer need Resource symbols or the this(..) return syntax — they're just async functions that return plain objects. Plus a new auth system in the CLI with profiles and OAuth, API token, or API key login for Cloudflare. +--- + +Alchemy 0.68 makes the biggest simplification to resource authoring since the project started: the `this({...})` return syntax is gone, output types no longer extend the `Resource` interface, and symbols are gone from our types entirely. A resource is now just an async function that returns a plain object. + +The CLI also gets a proper auth system for Cloudflare — `alchemy configure`, `alchemy login`, and `alchemy logout` manage named profiles with OAuth, API token, or Global API Key credentials, with no dependency on Wrangler's config. + + + +## Resources are just functions now + +Previously, a custom resource's output interface had to extend `Resource<"provider::Type">`, and the lifecycle function had to construct its result by calling `this(...)`: + +```ts +// before +export interface Database extends Resource<"neon::Database">, DatabaseProps { + id: string; + createdAt: number; +} + +export const Database = Resource( + "neon::Database", + async function (this: Context, id: string, props: DatabaseProps) { + if (this.phase === "delete") { + // ... + return this.destroy(); + } + // ... + return this({ id: db.id, createdAt: Date.now(), ...props }); + }, +); +``` + +Calling `this` as a function worked, but it surprised everyone who saw it. As of 0.68, you return a plain object, and the output type is just a type: + +```ts +// after +export interface Database extends DatabaseProps { + id: string; + createdAt: number; +} + +export const Database = Resource( + "neon::Database", + async function (this: Context, id: string, props: DatabaseProps) { + if (this.phase === "delete") { + // ... + return this.destroy(); + } + // ... + return { id: db.id, createdAt: Date.now(), ...props }; + }, +); +``` + +Alchemy still tags resources with symbols at runtime for state tracking — they're just no longer part of the types. That has a nice side effect: the symbol-based types were the source of gnarly typing errors when resources crossed package boundaries in monorepos, which matters a lot now that [apps can import each other](/guides/turborepo). Those errors are gone. + +If you author custom resources, this is a breaking change: replace `return this({...})` (or `this.create({...})`) with `return {...}`, and drop `Resource<"...">` from your output interfaces. See the [resource docs](/concepts/resource) for the current shape. + +## Log in to Cloudflare from the CLI + +The CLI now owns authentication end to end. Set up a profile with: + +```sh +alchemy configure +``` + +You pick a login method — OAuth (recommended), API token, or Global API Key — select the Cloudflare account, and the credentials are saved to a named profile under `~/.alchemy`. `alchemy login` runs the OAuth flow for a configured profile, and `alchemy logout` deletes (and, for OAuth, revokes) the saved credentials. + +Profiles are picked up automatically by Cloudflare resources — either explicitly or via environment variables: + +```ts +const worker = await Worker("api", { + entrypoint: "./src/index.ts", + profile: "my-team", // or CLOUDFLARE_PROFILE / ALCHEMY_PROFILE env vars +}); +``` + +This replaces the previous Wrangler-based OAuth flow and makes multi-account setups first-class: one profile per account or team, switched with an environment variable. Thanks **John Royal** for building this out. + +## Also in this release + +- **R2Bucket object methods.** `R2Bucket` now exposes `head`, `get`, `list`, `put`, and `delete` methods directly on the resource, so you can read and write objects from your `alchemy.run.ts` — handy for seeding buckets during deploys. See the [R2Bucket docs](/providers/cloudflare/bucket). +- **Neon Branch.** A new `NeonBranch` resource creates Neon branches with control over the parent branch, point-in-time restore via LSN or timestamp, schema-only initialization, expiration, and endpoints — and `NeonProject` was rewritten on Neon's generated API client. See the [Neon docs](/providers/neon/project). +- **Fixes.** WASM bundling no longer fails when a Worker is imported from a nested directory; internal workers are minified; remote bindings in local dev now run over capnweb; static assets are no longer stored in state (shrinking state files significantly); `bound.ts` preserves string literal types; WebSocket proxying was corrected in dev; and Docker builds enable BuildKit. + +## Thanks + +Thanks to **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.67.0...v0.68.0) for everything in 0.68. diff --git a/alchemy-web/src/content/docs/blog/2025-09-28-alchemy-0-69.md b/alchemy-web/src/content/docs/blog/2025-09-28-alchemy-0-69.md new file mode 100644 index 0000000000..7986247a22 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-09-28-alchemy-0-69.md @@ -0,0 +1,81 @@ +--- +title: "Alchemy 0.69: Cloud Control Replacement and Tail Consumers" +date: 2025-09-28 +authors: Sam Goodwin +excerpt: AWS Cloud Control resources now replace themselves automatically when an immutable property changes, and Cloudflare Workers gain tail consumers for declarative log routing — plus R2 data catalog support, an R2Object resource, and Docker container health checks. +--- + +Alchemy 0.69 makes the AWS Cloud Control provider behave the way you'd expect when you change a property that AWS won't let you update in place: instead of failing, the resource is replaced. On the Cloudflare side, Workers can now declare tail consumers, so routing logs from one Worker to another is a one-line prop instead of a dashboard exercise. + + + +## Automatic replacement in AWS Cloud Control + +The AWS Cloud Control provider generates resources for the entire CloudFormation schema — and that schema knows which properties are mutable and which are create-only. Alchemy now uses that information: when an update changes an immutable property, the resource is replaced instead of erroring mid-deploy. + +Take an SQS queue: + +```ts +import AWS from "alchemy/aws/control"; + +const queue = await AWS.SQS.Queue("jobs", { + QueueName: "jobs-queue", + VisibilityTimeout: 30, +}); +``` + +Changing `VisibilityTimeout` is a normal update. But flip the queue to FIFO: + +```ts +const queue = await AWS.SQS.Queue("jobs", { + QueueName: "jobs-queue.fifo", + FifoQueue: true, + VisibilityTimeout: 30, +}); +``` + +`FifoQueue` is immutable, so Cloud Control would previously reject the update. Now Alchemy detects the immutable change, creates the replacement, and deletes the old queue after your app finalizes — the same replace semantics the rest of Alchemy uses. + +:::caution +Alchemy doesn't generate unique physical names for Cloud Control resources. If you trigger a replacement without also changing the resource's name, the new resource will try to create with the same name as the old one and likely conflict — rename it in the same update. +::: + +## Tail Consumers for Workers + +[Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) receive the logs, exceptions, and events of another Worker. As of 0.69.1, wiring them up is a single prop: + +```ts +import { Worker } from "alchemy/cloudflare"; + +const tail = await Worker("log-tail", { + entrypoint: "./src/tail.ts", +}); + +export const api = await Worker("api", { + entrypoint: "./src/api.ts", + tailConsumers: [tail], +}); +``` + +`tailConsumers` accepts a `Worker` object or a `{ service: "worker-name" }` reference for Workers deployed outside Alchemy. The consumer just implements the `tail` handler: + +```ts +// src/tail.ts +export default { + async tail(events) { + // ship events to your logging system + }, +}; +``` + +## Also in this release + +- **R2 data catalog.** Set `dataCatalog: true` on an [R2Bucket](/providers/cloudflare/bucket) to enable the bucket's Apache Iceberg data catalog. +- **R2Object resource.** Manage individual objects inside an R2 bucket — text, JSON, or binary — as part of your deploy, with `httpMetadata` support for content types and cache control. See [R2Object](/providers/cloudflare/bucket-object). +- **Scheduled events in local dev.** Workers with `scheduled` handlers now run them in `alchemy dev`. +- **Docker container health checks.** The Docker `Container` resource accepts a `healthcheck` with `cmd`, `interval`, `timeout`, `retries`, and `startPeriod`. +- **Fixes.** Queue consumer `max_batch_timeout` is now emitted in `wrangler.json`, `build.command` and `dev.command` are optional on `Website`, R2 bucket methods work in dev mode, TanStack Start supports the latest RC, and the Neon provider no longer uses invalid `this(..)` syntax. + +## Thanks + +Thanks to **Jacob M-G Evans**, **John Royal**, **Leonardo E. Dominguez**, **Michael K**, **João Victor**, and **utopy** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.68.0...v0.69.1) for everything in 0.69. diff --git a/alchemy-web/src/content/docs/blog/2025-10-01-alchemy-0-70.md b/alchemy-web/src/content/docs/blog/2025-10-01-alchemy-0-70.md new file mode 100644 index 0000000000..96e566b751 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-10-01-alchemy-0-70.md @@ -0,0 +1,97 @@ +--- +title: "Alchemy 0.70: ClickHouse and Coinbase Providers" +date: 2025-10-01 +authors: Sam Goodwin +excerpt: Two new providers land in Alchemy — ClickHouse Cloud services and Coinbase Developer Platform accounts — alongside a --profile flag for the CLI, a Bun-native SPA resource, and typed Worker observability config. +--- + +Alchemy 0.70 adds two new providers. ClickHouse Cloud services are now a resource you can provision, scale, and wire into a Worker from one file. And the Coinbase Developer Platform provider brings blockchain accounts into Infrastructure-as-Code — EOAs and ERC-4337 smart accounts, managed like any other resource. + +The CLI also picked up a `--profile` flag, so switching between credential profiles no longer means juggling environment variables. + + + +## ClickHouse Cloud + +The new `Service` resource provisions a ClickHouse Cloud service with replica sizing, idle scaling, and IP access control — and hands you back typed endpoints plus the service password as a `Secret`: + +```ts +import alchemy from "alchemy"; +import { OrganizationRef, Service } from "alchemy/clickhouse"; +import { Worker } from "alchemy/cloudflare"; + +const app = await alchemy("my-app"); + +const organization = await OrganizationRef("my-organization"); + +const service = await Service("clickhouse", { + organization, + provider: "aws", + region: "us-east-1", + minReplicaMemoryGb: 8, + maxReplicaMemoryGb: 356, + numReplicas: 3, +}); + +const https = service.endpoints.find((e) => e.protocol === "https")!; + +export const worker = await Worker("api", { + entrypoint: "./src/worker.ts", + bindings: { + CLICKHOUSE_URL: `https://${https.host}:${https.port}`, + CLICKHOUSE_PASSWORD: service.password, + }, +}); + +await app.finalize(); +``` + +ClickHouse Cloud doesn't allow creating organizations via API, so `OrganizationRef` looks one up by name — or you can pass the organization name or ID directly as a string. From the Worker, connect with `@clickhouse/client-web` and the two bindings above. + +See the [Service docs](/providers/clickhouse/service) to get started. + +## Coinbase Developer Platform + +The `coinbase` provider manages EVM accounts on the [Coinbase Developer Platform](https://portal.cdp.coinbase.com/): plain EOAs and ERC-4337 smart accounts, with optional testnet faucet funding on create: + +```ts +import alchemy from "alchemy"; +import { EvmAccount, EvmSmartAccount } from "alchemy/coinbase"; + +const owner = await EvmAccount("owner", { + name: "my-wallet", + faucet: { + "base-sepolia": ["eth", "usdc"], + }, +}); + +const smartAccount = await EvmSmartAccount("smart", { + owner, +}); +``` + +You can also import an existing account by passing `privateKey: alchemy.secret(...)`. See the [Coinbase provider docs](/providers/coinbase/) for the full picture. + +## CLI profiles + +Every stateful CLI command now accepts `--profile` to pick which stored credentials to use (default: `default`): + +```sh +alchemy deploy --profile prod +alchemy dev --profile dev +alchemy destroy --profile prod +alchemy run --profile prod +``` + +## Also in this release + +- **BunSPA.** A new resource that builds single-page apps with Bun's native bundler — HMR in dev, `bun build` on deploy — with an optional Worker backend. See [BunSPA](/providers/cloudflare/bun-spa). +- **Worker Logpush.** Set `logpush: true` on a Worker to export trace events to your Logpush jobs. +- **Typed observability config.** The Worker `observability` prop is now fully typed, including `logs` and `traces` sampling configuration (0.70.2). +- **More container instance types.** Cloudflare Containers accept `lite` and `standard-1` through `standard-4` instance types. +- **Global telemetry opt-out.** `alchemy telemetry disable` turns off telemetry machine-wide (0.70.1). +- **Fixes.** The Vite dev server no longer hangs, `dev.command` is optional on `Website`, D1 migrations run on fresh installs in local dev, Docker socket paths have better defaults for Containers, PlanetScale API errors are no longer swallowed, and Worker version bindings are cleaned up on delete. + +## Thanks + +Thanks to **Andrew Jefferson**, **Jacob M-G Evans**, **John Royal**, **Jordan Coeyman**, **Michael K**, **Nick Balestra-Foster**, and **utopy** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.69.1...v0.70.2) for everything in 0.70. diff --git a/alchemy-web/src/content/docs/blog/2025-10-08-alchemy-0-71.md b/alchemy-web/src/content/docs/blog/2025-10-08-alchemy-0-71.md new file mode 100644 index 0000000000..ececd0f243 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-10-08-alchemy-0-71.md @@ -0,0 +1,113 @@ +--- +title: "Alchemy 0.71: Dynamic Worker Loaders and Health Checks" +date: 2025-10-08 +authors: Sam Goodwin +excerpt: Workers can now load and run other Workers at runtime with the new WorkerLoader binding, Cloudflare Health Checks monitor your origins declaratively, and the LogPushJob resource streams logs to S3, R2, Datadog, and friends. +--- + +Alchemy 0.71 is a Cloudflare release. The new `WorkerLoader` binding lets a Worker spin up other Workers dynamically at runtime — the primitive behind per-tenant sandboxing and running user-supplied code. Alongside it, two new resources: `HealthCheck` for origin monitoring and `LogPushJob` for declarative log pipelines. + + + +## Dynamic Worker Loaders + +[Dynamic Worker Loaders](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/) let a Worker define and execute other Workers on the fly, each in its own isolate. Binding one is now a one-liner: + +```ts +import { Worker, WorkerLoader } from "alchemy/cloudflare"; + +export const worker = await Worker("host", { + entrypoint: "./src/worker.ts", + bindings: { + LOADER: WorkerLoader(), + }, +}); +``` + +At runtime, hand the loader a name and a module graph, and it gives you back a live Worker: + +```ts +// src/worker.ts +import type { worker } from "../alchemy.run.ts"; + +export default { + async fetch(request: Request, env: typeof worker.Env) { + const dynamicWorker = env.LOADER.get("my-dynamic-worker", async () => ({ + compatibilityDate: "2025-06-01", + mainModule: "index.js", + modules: { + "index.js": ` + export default { + async fetch(request) { + return new Response("Hello from a dynamic worker!"); + } + } + `, + }, + })); + + const entrypoint = dynamicWorker.getEntrypoint(); + return entrypoint.fetch(new URL(request.url)); + }, +}; +``` + +The isolate boundary means the loaded code gets its own compatibility date and bindings — nothing from the host leaks in unless you pass it. + +## Health Checks + +The new `HealthCheck` resource (0.71.1) manages [Cloudflare Health Checks](https://developers.cloudflare.com/load-balancing/monitors/), which probe your origin servers and mark them healthy or unhealthy — pair them with load balancers for automatic failover: + +```ts +import { HealthCheck } from "alchemy/cloudflare"; + +const check = await HealthCheck("api-health", { + zone: "023e105f4ecef8ad9ca31a8372d0c353", + address: "api.example.com", + name: "api-server-check", + type: "HTTPS", + interval: 30, + consecutiveFails: 2, + consecutiveSuccesses: 2, + httpConfig: { + path: "/health", + expectedCodes: ["200"], + }, +}); +``` + +HTTP, HTTPS, and TCP checks are supported, with control over timing, thresholds, expected responses, and which regions probe your origin. See the [Health Check docs](/providers/cloudflare/health-check). + +## Logpush Jobs + +`LogPushJob` streams Cloudflare logs — HTTP requests, firewall events, DNS, Workers trace events — to S3, R2, GCS, Datadog, and other destinations, with filtering and sampling in code: + +```ts +import { LogPushJob } from "alchemy/cloudflare"; + +const httpLogs = await LogPushJob("http-logs", { + zone: "example.com", + dataset: "http_requests", + destinationConf: "s3://my-bucket/logs?region=us-west-2", +}); + +const blockedRequests = await LogPushJob("security", { + zone: "example.com", + dataset: "firewall_events", + destinationConf: "r2://logs/firewall", + filter: '{"where":{"and":[{"key":"Action","operator":"eq","value":"block"}]}}', + sample: 0.1, +}); +``` + +This pairs with the `logpush: true` Worker prop from 0.70 — the Worker emits trace events, and the job decides where they land. See the [Logpush docs](/providers/cloudflare/logpush). + +## Also in this release + +- **BunSPA multiple entrypoints.** The `frontend` prop now accepts an array of HTML files to serve multiple pages. +- **Test harness export.** The vitest-based `alchemy.test` harness Alchemy uses internally is now published as `alchemy/test/vite`, so you can write resource lifecycle tests the same way. +- **Fixes.** Remote bindings set the `duplex` property and respect R2 bucket jurisdictions, dead letter queues are handled correctly, assets resolve relative to `cwd` in dev mode, the container engine only starts when containers are actually used, `wrangler.json` uses `remote` instead of `experimental_remote`, empty resource and scope IDs are rejected with a clear error, and GitHub resources no longer authenticate during deletion unless required. + +## Thanks + +Thanks to **Andrew Jefferson**, **Gareth Andrew**, **Jacob M-G Evans**, **John Royal**, and **Michael K** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.70.2...v0.71.1) for everything in 0.71. diff --git a/alchemy-web/src/content/docs/blog/2025-10-09-alchemy-0-72.md b/alchemy-web/src/content/docs/blog/2025-10-09-alchemy-0-72.md new file mode 100644 index 0000000000..80cf964c8b --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-10-09-alchemy-0-72.md @@ -0,0 +1,48 @@ +--- +title: "Alchemy 0.72: PlanetScale Organization Refs" +date: 2025-10-09 +authors: Sam Goodwin +excerpt: PlanetScale resources now take object references instead of raw ID strings — pass a Database to a Branch and the organization is inferred, or look one up with OrganizationRef. +--- + +Alchemy 0.72 is a small release focused on the PlanetScale provider: resources now reference each other as objects instead of raw ID strings, matching the convention used across the rest of Alchemy. + + + +## Object references for PlanetScale + +Previously, PlanetScale resources took `organizationId` and `databaseName` strings, which meant repeating IDs everywhere. Now they take `organization` and `database`, and each accepts either a string or the resource object itself — pass the object and downstream values are inferred: + +```ts +import { Branch, Database } from "alchemy/planetscale"; + +const database = await Database("db", { + name: "my-app-db", + organization: "my-org", + clusterSize: "PS_10", +}); + +const branch = await Branch("dev", { + name: "dev", + database, // organization inferred from the database + parentBranch: "main", +}); +``` + +There's also a new `OrganizationRef` to look up an organization by name and reuse it across resources: + +```ts +import { OrganizationRef } from "alchemy/planetscale"; + +const organization = await OrganizationRef("my-org"); +``` + +:::note +This renames `organizationId` to `organization` and `databaseName` to `database` on PlanetScale props — update your `alchemy.run.ts` accordingly. Existing state is handled automatically, so previously created resources still destroy cleanly. +::: + +See the [PlanetScale docs](/providers/planetscale/) for details. + +## Thanks + +Thanks to **Michael K** for contributing this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.71.1...v0.72.0). diff --git a/alchemy-web/src/content/docs/blog/2025-10-15-alchemy-0-74.md b/alchemy-web/src/content/docs/blog/2025-10-15-alchemy-0-74.md new file mode 100644 index 0000000000..e5ab0eb0ae --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-10-15-alchemy-0-74.md @@ -0,0 +1,84 @@ +--- +title: "Alchemy 0.74: Prisma Postgres and Cloudflare God Tokens" +date: 2025-10-15 +authors: Sam Goodwin +excerpt: Prisma Postgres is now a first-class provider in Alchemy — projects, databases, and connections in one TypeScript file. Plus a new CLI utility for minting Cloudflare API tokens, including a full-access "god token" for CI. +--- + +Alchemy 0.74 adds a new provider for [Prisma Postgres](https://www.prisma.io/postgres) — projects, databases, and scoped connection strings, all defined in your `alchemy.run.ts` and wired straight into a Cloudflare Worker through Hyperdrive. + +It also ships a CLI utility that solves one of the most annoying parts of using Cloudflare in CI: creating an API token with the right scopes. One command now mints a token for you — either mirroring your OAuth profile's scopes or, if you'd rather never think about scopes again, a "god token" with full write access to your account. + + + +## Prisma Postgres + +The new `alchemy/prisma-postgres` provider gives you four resources: `Project`, `Database`, `Connection`, and `WorkspaceRef`. Here's a complete setup — a database in `us-east-1`, a connection string, a Hyperdrive in front of it, and a Worker that queries it: + +```ts +import alchemy from "alchemy"; +import { Hyperdrive, Worker } from "alchemy/cloudflare"; +import { Connection, Database, Project } from "alchemy/prisma-postgres"; + +const app = await alchemy("my-app"); + +const project = await Project("project"); + +const database = await Database("database", { + project, + region: "us-east-1", +}); + +const connection = await Connection("connection", { + database, + name: "connection", +}); + +const hyperdrive = await Hyperdrive("db", { + origin: connection.connectionString.unencrypted, +}); + +export const worker = await Worker("api", { + entrypoint: "./src/worker.ts", + compatibilityFlags: ["nodejs_compat"], + bindings: { + HYPERDRIVE: hyperdrive, + }, +}); + +console.log(`worker url: ${worker.url}`); + +await app.finalize(); +``` + +`connection.connectionString` is a `Secret` — encrypted at rest in your state file, unwrapped only when Hyperdrive needs it. Authentication is a single `PRISMA_SERVICE_TOKEN` environment variable, with a per-resource `serviceToken` override if you manage multiple accounts. + +See the [Prisma Postgres docs](/providers/prisma-postgres/) to get started. + +## Cloudflare token minting + +Creating a Cloudflare API token by hand means clicking through the dashboard and guessing which of the dozens of permission groups your deploy actually needs. The CLI now does it for you: + +```sh +bun alchemy util create-cloudflare-token --profile +``` + +This creates a token mirroring the OAuth scopes of the given profile — the least-privilege option. + +If you want a token that will never fail on a missing scope, there's the nuclear option: + +```sh +bun alchemy util create-cloudflare-token --god-token +``` + +This mints a token with full write access to everything in your Cloudflare account. Drop it into `CLOUDFLARE_API_TOKEN` in your CI secrets and every Alchemy resource — Workers, R2, D1, DNS, Zero Trust — just works. Use it for CI environments where you control the secret; prefer the profile-scoped variant everywhere else. + +## Also in this release + +- **PlanetScale resources can opt out of deletion.** `Database`, `Branch`, `Password`, and `Role` now accept `delete: false`, so destroying your Alchemy app removes them from state without deleting the underlying database. Useful when Alchemy adopts a production database you'd rather not be one `destroy` away from losing. +- **Optional bindings.** Worker bindings typed as `T | undefined` now resolve correctly through `Bound`, so you can conditionally include bindings without fighting the type system. +- **Fixes.** Improved WebSocket proxy handling in local dev, auto-detection of the server entry for TanStack Start, the Vite plugin no longer watches `.alchemy` (ending reload loops), Miniflare symlinks now work in split monorepos, and the Docker `Volume` resource no longer mutates its input props. + +## Thanks + +Thanks to **Michael K**, **Rahul Mishra**, and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.73.1...v0.74.2) for everything included. diff --git a/alchemy-web/src/content/docs/blog/2025-10-31-alchemy-0-77.md b/alchemy-web/src/content/docs/blog/2025-10-31-alchemy-0-77.md new file mode 100644 index 0000000000..ea7fef03eb --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-10-31-alchemy-0-77.md @@ -0,0 +1,83 @@ +--- +title: "Alchemy 0.77: Containers on the Official Wrangler Spec" +date: 2025-10-31 +authors: Sam Goodwin +excerpt: Container bindings now work end-to-end and the generated wrangler.json follows the official wrangler spec. Also in this line — Cloudflare Access support for remote bindings, R2 bucket custom domains, D1 jurisdictions, pre-built Container images, and stronger secret encryption. +--- + +The 0.77 line is mostly about closing gaps between Alchemy and the wider Cloudflare toolchain. The `wrangler.json` that Alchemy generates for local dev is now built on wrangler's own config types instead of a hand-maintained mirror of them — which fixes Container bindings in local dev and keeps every other binding type honest by construction. + +Beyond that: remote bindings now work against Workers protected by Cloudflare Access, R2 buckets can attach custom domains directly, D1 databases gained a `jurisdiction` property, Containers can run pre-built images without a local Docker build, and new secrets are encrypted with AES-256-GCM. + + + +## Containers on the official wrangler spec + +Alchemy generates a `wrangler.json` under `.alchemy/local/` so that Vite and wrangler-based tooling can run your Worker locally with the right bindings. That file used to be produced from Alchemy's own hand-written types — nearly 400 lines of them — which drifted from what wrangler actually accepts. Container bindings were the biggest casualty: they were emitted without the fields wrangler needs, so Containers didn't work in local dev. + +The generator is now typed against wrangler's own `Unstable_Config`, and Container bindings are emitted with their class name and resolved image — the local build path when you build from a Dockerfile, or the fully-qualified Cloudflare registry reference otherwise: + +```ts +import { Container, Worker } from "alchemy/cloudflare"; +import type { MyContainer } from "./src/container.ts"; + +const container = await Container("my-container", { + className: "MyContainer", +}); + +export const worker = await Worker("api", { + entrypoint: "./src/index.ts", + bindings: { + MY_CONTAINER: container, + }, +}); +``` + +No user-facing API changed — the same code just produces a correct `wrangler.json` now, and rate limit bindings moved out of `unsafe.bindings` into the proper `ratelimits` field along the way. + +## Cloudflare Access for remote bindings + +Remote bindings work by deploying a preview Worker to your account and proxying binding calls to it from local dev. If your `workers.dev` subdomain sits behind [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/), that proxy used to hit the Access login wall and fail. + +Alchemy now detects when the preview host redirects to Access, obtains a token via `cloudflared access login`, and attaches the `CF_Authorization` cookie to every proxied request — HTTP and WebSocket alike. If the domain isn't behind Access, nothing changes; if it is, the only requirement is having the [`cloudflared` CLI](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation) installed. Zero-trust shops no longer have to choose between Access and `remote: true` bindings. + +## R2 bucket custom domains + +R2 buckets have long supported the rate-limited `r2.dev` subdomain for development, but production access meant clicking through the dashboard to attach a domain. Now it's a property: + +```ts +import { R2Bucket } from "alchemy/cloudflare"; + +const bucket = await R2Bucket("assets", { + name: "assets", + domains: "assets.example.com", +}); +``` + +`domains` accepts a single domain, an array, or option objects when you need control over TLS and enablement: + +```ts +const bucket = await R2Bucket("assets", { + name: "assets", + domains: [ + { domain: "assets.example.com", minTLS: "1.2" }, + { domain: "cdn.example.com" }, + ], +}); +``` + +The domain must belong to a zone on the same Cloudflare account. As part of this change, the old `allowPublicAccess` property is deprecated in favor of `devDomain: true`, with the resulting `r2.dev` hostname exposed as `bucket.devDomain`. See the [R2 Bucket docs](/providers/cloudflare/bucket) for details. + +## Also in this release + +- **D1 jurisdictions.** `D1Database` accepts a `jurisdiction` property to pin database data to a regulatory jurisdiction. Jurisdiction is immutable — Alchemy errors if you try to change it on an existing database. See the [D1 docs](/providers/cloudflare/d1-database). +- **Pre-built images for Containers.** `Container` now takes an `image` property — a registry reference like `"alpine:latest"`, a built `Image`, or a `RemoteImage` — so you can deploy Containers without building a Dockerfile on every deploy. See the [Container docs](/providers/cloudflare/container). +- **Stronger secret encryption.** New secrets in state files are encrypted with AES-256-GCM using scrypt-derived keys via Node's built-in crypto. Existing libsodium-encrypted secrets still decrypt transparently. +- **Docker build cache exports.** `Image` gained `build.cacheTo` for exporting build cache (great for CI) and a `build.options` escape hatch for any other `docker buildx build` flags. +- **AWS exports.** All AWS resources are now exported, including the `aws/ec2` module. +- **CLI env files.** The CLI now falls back to the default `.env` file when `--env-file` isn't specified. +- **Fixes.** esbuild errors no longer crash the dev server reload, Worker sourcemaps are correctly formatted, React Router works with rolldown-vite, adopting a non-Alchemy Worker now handles existing container bindings, PlanetScale Postgres roles get a default successor, and telemetry times out after one second instead of hanging a deploy. + +## Thanks + +Thanks to **Rahul Mishra**, **John Royal**, **Michael K**, and **Erik Müller** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.76.1...v0.77.5) for everything included. diff --git a/alchemy-web/src/content/docs/blog/2025-12-04-alchemy-0-80.md b/alchemy-web/src/content/docs/blog/2025-12-04-alchemy-0-80.md new file mode 100644 index 0000000000..d3a609e692 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-12-04-alchemy-0-80.md @@ -0,0 +1,97 @@ +--- +title: "Alchemy 0.80: Bind a Worker's Own URL" +date: 2025-12-04 +authors: Sam Goodwin +excerpt: Worker.DevDomain and Worker.DevUrl are new bindings that inject a Worker's own workers.dev domain or URL into its script — and resolve to localhost during local dev, with the port inferred from your vite.config.ts. Docker Container and Volume also gained adopt support. +--- + +A Worker often needs to know its own URL — to build absolute links, configure OAuth callbacks, or point a compiled SPA at its API. Until now that meant hardcoding the `workers.dev` hostname or threading it through as a plain-text binding, and it broke the moment you switched to local dev. Alchemy 0.80 adds `Worker.DevDomain` and `Worker.DevUrl`: bindings that resolve to the Worker's own dev domain when deployed and to `localhost:{port}` when running locally. + +The line also brings `adopt` support to the Docker provider's `Container` and `Volume` resources, and to `NeonProject`. + + + +## Worker.DevDomain and Worker.DevUrl + +Every Worker with `url: true` (the default) gets a `workers.dev` subdomain computed from its name and your account's worker subdomain: `${workerName}.${subdomain}.workers.dev`. The new bindings inject that value into the Worker itself: + +```ts +import { Worker } from "alchemy/cloudflare"; + +const worker = await Worker("api", { + name: "api-worker", + entrypoint: "./src/api.ts", + bindings: { + // api-worker..workers.dev + DEV_DOMAIN: Worker.DevDomain, + // https://api-worker..workers.dev + DEV_URL: Worker.DevUrl, + }, +}); +``` + +Both bind as plain strings — `env.DEV_URL` in your script, no lookup, no hardcoding. + +The part that makes this actually useful: under `alchemy dev`, the same bindings resolve to your local server instead — `localhost:{port}` and `http://localhost:{port}` — so code that reads `env.DEV_URL` works identically in both environments. + +It also works in `Website`-based resources like `Vite`, where it solves the classic "what URL does my SPA call?" problem — the binding is injected into the compiled client build: + +```ts +import { Vite, Worker } from "alchemy/cloudflare"; + +const app = await Vite("my-vite-app", { + name: "my-vite-app", + bindings: { + VITE_PUBLIC_URL: Worker.DevUrl, + }, +}); +``` + +### Local port inference + +For the local URL to be right, Alchemy needs to know which port your dev server runs on. As of 0.80.1, `Vite` figures it out automatically: + +1. If `dev.command` contains `--port`, that port wins. +2. Otherwise Alchemy imports your `vite.config.ts` and reads `server.port`, defaulting to `5173`. +3. If the config can't be imported, you'll get an error asking you to set `dev.domain` explicitly: + +```ts +const app = await Vite("my-vite-app", { + // ... + dev: { + domain: "localhost:5006", + }, +}); +``` + +See the [Worker](/providers/cloudflare/worker) and [Vite](/providers/cloudflare/vite) docs for more. + +## Adopt existing Docker Containers and Volumes + +The Docker provider's `Container` and `Volume` resources now support the standard Alchemy `adopt` pattern. If a container or volume with the same name already exists, Alchemy takes ownership of it instead of failing: + +```ts +import { Container, Volume } from "alchemy/docker"; + +const data = await Volume("data", { + name: "postgres-data", + adopt: true, +}); + +const db = await Container("db", { + image: "postgres:17", + name: "postgres", + adopt: true, + start: true, +}); +``` + +On adoption, Alchemy inspects the existing container, records its state, and — if you pass `start: true` — starts it if it isn't already running. Without `adopt: true`, a name conflict is still an error, same as every other Alchemy resource. See the [Container](/providers/docker/container) and [Volume](/providers/docker/volume) docs. + +## Also in this release + +- **NeonProject supports `adopt`.** Adopt an existing Neon project by name instead of erroring on conflict — and pair it with `delete: false` if the project should outlive the Alchemy app. A read-only `NeonProjectRef` is available when you don't want ownership at all. See the [Neon docs](/providers/neon/project). + +## Thanks + +Thanks to **David Mo**, **Eric Clemmons**, and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.79.0...v0.80.1) for everything included. diff --git a/alchemy-web/src/content/docs/blog/2025-12-10-alchemy-0-81.md b/alchemy-web/src/content/docs/blog/2025-12-10-alchemy-0-81.md new file mode 100644 index 0000000000..d0c4870211 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-12-10-alchemy-0-81.md @@ -0,0 +1,89 @@ +--- +title: "Alchemy 0.81: WARP Device Profiles, Tunnel Routes, and Hyperdrive Cache Control" +date: 2025-12-10 +authors: Sam Goodwin +excerpt: Cloudflare Zero Trust private networking comes to Alchemy with WARP device profiles and CIDR tunnel routes, and Hyperdrive gains cache tuning fields plus a delete-protection option. +--- + +Alchemy 0.81 extends the Cloudflare Zero Trust story with three new resources — `WarpDeviceProfile`, `WarpDefaultProfile`, and `TunnelRoute` — so you can route private networks through Cloudflare Tunnels and control WARP client behavior from your `alchemy.run.ts`. + +Hyperdrive also picked up the cache tuning fields it was missing (`max_age`, `stale_while_revalidate`) and a `delete: false` option for configs you want to outlive your Alchemy state. + + + +## Private networks over Cloudflare Tunnels + +`TunnelRoute` routes a private CIDR range through a Cloudflare Tunnel, and `WarpDeviceProfile` controls how WARP clients on your devices reach it — matching rules, split tunnels, service modes: + +```ts +import { Tunnel, TunnelRoute, WarpDeviceProfile } from "alchemy/cloudflare"; + +const tunnel = await Tunnel("vpc-tunnel", { + name: "vpc-tunnel", +}); + +const route = await TunnelRoute("vpc-route", { + network: "10.0.0.0/8", + tunnel, + comment: "Main VPC network route", +}); + +const profile = await WarpDeviceProfile("engineering", { + name: "Engineering Team", + match: 'identity.groups.name == "Engineering"', + precedence: 100, + serviceModeV2: { mode: "warp" }, + splitTunnel: { + mode: "include", + entries: [{ address: "10.0.0.0/8", description: "Company network" }], + }, +}); +``` + +Point a `cloudflared` connector at the tunnel and devices in the Engineering group can reach `10.0.0.0/8` over Cloudflare's network — no VPN appliance, no clicking through the Zero Trust dashboard. There's also `WarpDefaultProfile` for managing the account-wide default WARP settings as a resource. + +`TunnelRoute` supports `adopt: true` for taking over existing routes and `delete: false` if you want the route to survive a `destroy`. See the [TunnelRoute](/providers/cloudflare/tunnel-route) and [WarpDeviceProfile](/providers/cloudflare/warp-device-profile) docs. + +## Hyperdrive cache control + +Hyperdrive's `caching` property previously only let you turn caching off. It now exposes the full set of fields from the Cloudflare API, so you can tune query cache behavior directly: + +```ts +import { Hyperdrive } from "alchemy/cloudflare"; + +const hyperdrive = await Hyperdrive("db", { + origin: { + database: "postgres", + host: "database.example.com", + user: "postgres", + password: alchemy.secret.env.DB_PASSWORD, + }, + caching: { + max_age: 120, // cache items for up to 2 minutes (default 60s) + stale_while_revalidate: 30, // serve stale results for up to 30s (default 15s) + }, +}); +``` + +The types are a discriminated union: `caching: { disabled: true }` still works, and TypeScript will stop you from combining `disabled: true` with the tuning fields. + +Hyperdrive also gained `delete: false` (in 0.81.4), matching the pattern on other data resources — when set, removing the resource from Alchemy leaves the Hyperdrive config running in Cloudflare and just drops it from state: + +```ts +const hyperdrive = await Hyperdrive("db", { + origin: alchemy.secret.env.DATABASE_URL, + delete: false, // keep the config in Cloudflare on destroy +}); +``` + +See the [Hyperdrive docs](/providers/cloudflare/hyperdrive) for the full set of options. + +## Also in this release + +- **Hyperdrive in generated API tokens.** API tokens generated by the Alchemy CLI with the `workers:*` scopes now include Hyperdrive permissions, so Hyperdrive deploys work out of the box. +- **`dev.domain` in Next.js.** The Next.js integration now supports the `dev.domain` option. +- **Fixes.** Safe user headers are passed through to miniflare websockets, Cloudflare queue lookup now paginates past the first page, `RedirectRule` resolves `zoneId` correctly and uses the individual Rule APIs, Next.js 16 absolute paths in wasm imports are handled, `destroy` no longer waits for consistent state, the root Scope is always root in monorepos, Prisma Postgres builds `connectionString` properly, and `Website` resources lowercase worker names as Cloudflare requires. + +## Thanks + +Thanks to **John Royal**, **Andrew Jefferson**, **Riley Tomasek**, **CharlieHelps**, **Yanqi Zong**, and **Michael K** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.80.1...v0.81.4). diff --git a/alchemy-web/src/content/docs/blog/2025-12-19-alchemy-0-82.md b/alchemy-web/src/content/docs/blog/2025-12-19-alchemy-0-82.md new file mode 100644 index 0000000000..83a922c489 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2025-12-19-alchemy-0-82.md @@ -0,0 +1,54 @@ +--- +title: "Alchemy 0.82: D1 Imports and Neon Branch Adoption" +date: 2025-12-19 +authors: Sam Goodwin +excerpt: D1Database can now import SQL files through Cloudflare's D1 import API — seeding included in your deploy — and Neon branches can adopt existing branches by name instead of failing on conflict. +--- + +Alchemy 0.82 is a focused release with two quality-of-life features for databases: `D1Database` gained an `importFiles` property that runs SQL files through [Cloudflare's D1 import API](https://developers.cloudflare.com/d1/best-practices/import-export-data/) as part of your deploy, and `NeonBranch` now supports `adopt: true` so re-running your app against an existing branch takes it over instead of erroring. + + + +## Import SQL into D1 + +Seeding a D1 database used to mean shelling out to `wrangler d1 execute` after your deploy. Now it's a property: + +```ts +import { D1Database } from "alchemy/cloudflare"; + +const db = await D1Database("db", { + migrationsDir: "./migrations", + importFiles: ["./seed.sql"], +}); +``` + +Migrations are applied first, then each file in `importFiles` is uploaded and ingested through the D1 import API — the same mechanism `wrangler d1 execute --remote --file` uses, which handles large SQL files far better than statement-by-statement execution over HTTP. + +This works in `alchemy dev` too: import files are applied to the local miniflare database alongside your migrations, so local and deployed databases are seeded the same way. See the [D1Database docs](/providers/cloudflare/d1-database). + +## Adopt existing Neon branches + +`NeonBranch` now accepts an `adopt` prop. When the branch name already exists in the project, Alchemy adopts it — reading its endpoints, databases, and roles into state — rather than failing on the name conflict: + +```ts +import { NeonBranch, NeonProject } from "alchemy/neon"; + +const project = await NeonProject("project", {}); + +const branch = await NeonBranch("preview", { + project, + name: "preview", + adopt: true, + endpoints: [{ type: "read_write" }], +}); +``` + +This is the same adoption pattern the Cloudflare resources use (and it respects the scope-level adopt flag), which makes Neon branches practical for preview environments: the first deploy creates the branch, subsequent deploys from the same PR pick it up. + +## Also in this release + +- **Fixes.** D1 database creation no longer fails with a malformed request, local D1 migrations no longer fail with "column already exists" on re-runs, and `drizzle-orm` is now a proper dependency of the state store. + +## Thanks + +Thanks to **utopy** and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.81.4...v0.82.2). diff --git a/alchemy-web/src/content/docs/blog/2026-01-04-alchemy-0-83.md b/alchemy-web/src/content/docs/blog/2026-01-04-alchemy-0-83.md new file mode 100644 index 0000000000..0f7e4466eb --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-01-04-alchemy-0-83.md @@ -0,0 +1,96 @@ +--- +title: "Alchemy 0.83: Container Rollouts and Worker Placement Hints" +date: 2026-01-04 +authors: Sam Goodwin +excerpt: Containers can now roll out updates gradually with a configurable rollout strategy, Workers gained region, host, and hostname placement hints, and Hyperdrive supports dev.remote for running against the real config in local dev. +--- + +Alchemy 0.83 is about controlling where and how your Cloudflare compute runs. `Container` gained a `rollout` property for progressive deployments — update 25% of instances at a time instead of all at once. `Worker` gained placement hints beyond smart placement: pin your Worker near a specific cloud region, a TCP endpoint, or an HTTP hostname. And `Hyperdrive` picked up `dev: { remote: true }` for developing against the real config instead of a local emulation. + + + +## Progressive Container rollouts + +By default, updating a `Container` now rolls the new configuration out gradually — 25% of instances per step, fully automated. You can tune the step size or opt into immediate rollouts: + +```ts +import { Container } from "alchemy/cloudflare"; +import type { MyContainer } from "./src/container.ts"; + +const container = await Container("my-container", { + className: "MyContainer", + rollout: { + strategy: "rolling", // default + stepPercentage: 25, // update 25% of instances per step + }, +}); +``` + +If you'd rather flip everything at once: + +```ts +const container = await Container("my-container", { + className: "MyContainer", + rollout: { strategy: "immediate" }, +}); +``` + +The target configuration — image, instance type, observability — is derived from the container itself, so there's nothing else to specify. See the [Container docs](/providers/cloudflare/container). + +## Worker placement hints + +Smart placement lets Cloudflare pick where your Worker runs based on traffic patterns. But sometimes you already know the answer: your database lives in `us-east-1` and the Worker should run next to it. The `placement` property now supports region, host, and hostname hints alongside `mode: "smart"`: + +```ts +import { Worker } from "alchemy/cloudflare"; + +// Place the Worker closest to a cloud provider region +const worker = await Worker("api", { + entrypoint: "./src/worker.ts", + placement: { region: "aws:us-east-1" }, +}); +``` + +If your infrastructure isn't in a major cloud, you can have Cloudflare probe an endpoint instead — a TCP host (layer 4, e.g. a database) or an HTTP hostname (layer 7): + +```ts +// Probe a TCP endpoint (e.g. a database) for optimal placement +const worker = await Worker("api", { + entrypoint: "./src/worker.ts", + placement: { host: "my-database.example.com:5432" }, +}); + +// Or probe an HTTP endpoint +const proxy = await Worker("proxy", { + entrypoint: "./src/proxy.ts", + placement: { hostname: "my-api.example.com" }, +}); +``` + +The options are mutually exclusive — the types enforce exactly one. Region hints support AWS (`aws:us-east-1`), GCP (`gcp:us-east4`), and Azure (`azure:westeurope`) regions. See the [Worker docs](/providers/cloudflare/worker). + +## Hyperdrive in remote dev + +`alchemy dev` normally emulates Hyperdrive locally by connecting straight to your `dev.origin`. If you want your local Worker to go through the real Hyperdrive config — connection pooling, caching, and all — set `dev.remote`: + +```ts +import { Hyperdrive } from "alchemy/cloudflare"; + +const hyperdrive = await Hyperdrive("db", { + origin: alchemy.secret.env.DATABASE_URL, + dev: { + remote: true, + }, +}); +``` + +This mirrors the `dev.remote` option already available on resources like D1, so you can choose per-resource which parts of local dev are emulated and which are live. + +## Also in this release + +- **`--erase-secrets` escape hatch.** If your state file contains secrets you can no longer decrypt (a lost password, an encryption bug), `--erase-secrets --force` skips decryption and treats them as undefined so you can deploy or destroy your way out instead of being locked out of your own state. +- **Fixes.** OAuth errors are no longer intercepted in Cloudflare API error handling, "durable object already has application" detection was updated, `LogPushJob` error messages are clearer, queue consumer JSDoc reflects actual defaults, the Astro integration ignores the `.alchemy` directory, Next.js deploys use hashed paths for wasm imports, multiple `set-cookie` headers survive the miniflare-to-Node response bridge, and the `destroy` command description was corrected. + +## Thanks + +Thanks to **Yanqi Zong**, **John Royal**, **Jan Henning**, **Edwin Tantawi**, **Sergey Bekrin**, and **Samson** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.82.2...v0.83.3). diff --git a/alchemy-web/src/content/docs/blog/2026-02-18-alchemy-0-85.md b/alchemy-web/src/content/docs/blog/2026-02-18-alchemy-0-85.md new file mode 100644 index 0000000000..db1081e68b --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-02-18-alchemy-0-85.md @@ -0,0 +1,116 @@ +--- +title: "Alchemy 0.85: Cloudflare AI Search" +date: 2026-02-18 +authors: Sam Goodwin +excerpt: Cloudflare AI Search (formerly AutoRAG) is now a first-class Alchemy resource — point it at an R2 bucket and get a managed RAG pipeline you can query from a Worker binding. This release also adds VPC Services for reaching private networks from Workers and a configurable subrequest limit. +--- + +Alchemy 0.85 brings [Cloudflare AI Search](https://developers.cloudflare.com/ai-search/) to your `alchemy.run.ts`. Define an AI Search instance, point it at an R2 bucket (or a web crawler, or nothing at all), and query it from a Worker — indexing, embeddings, and retrieval are all managed by Cloudflare. + +The release also ships `VpcService` for reaching private networks from Workers through a Cloudflare Tunnel, and a `limits.subrequests` option for Workers that make a lot of outbound calls. + + + +## AI Search + +AI Search (formerly AutoRAG) indexes your data, generates vector embeddings, and answers natural-language queries with AI-generated responses — a managed RAG pipeline without the pipeline. The new `AiSearch` resource stands one up in a few lines. Pass an R2 bucket directly as the source and Alchemy handles the service token wiring for you: + +```ts +import alchemy from "alchemy"; +import { AiSearch, R2Bucket, Worker } from "alchemy/cloudflare"; + +const app = await alchemy("my-app"); + +const bucket = await R2Bucket("docs", { name: "my-docs" }); + +const search = await AiSearch("docs-search", { + source: bucket, +}); + +export const worker = await Worker("api", { + entrypoint: "./src/worker.ts", + bindings: { + DOCS: search, + }, +}); + +await app.finalize(); +``` + +The binding gives your Worker first-class access to the search, chat, items, and jobs APIs: + +```ts +// src/worker.ts +export default { + async fetch(_req: Request, env: Env) { + const results = await env.DOCS.search({ + query: "How does caching work?", + }); + return Response.json(results); + }, +}; +``` + +Sources are flexible: an R2 bucket (shorthand as above, or the full `{ type: "r2", bucket, prefix, includePaths, excludePaths }` form), a web crawler (`{ type: "web-crawler", domain }`), or no source at all — which gives you built-in storage you can upload to at runtime via `env.DOCS.items.upload(name, content)`. + +The retrieval pipeline is tunable through props: chunking (`chunkSize`, `chunkOverlap`), hybrid vector + keyword search (`indexMethod`), reranking, query rewriting, and similarity caching. Instances can also be organized into namespaces with the new `AiSearchNamespace` resource — bind the namespace to a Worker and grab instances dynamically with `env.NS.get(name)`, which is handy for per-tenant search. + +Get started: [AiSearch](/providers/cloudflare/ai-search) · [AiSearchNamespace](/providers/cloudflare/ai-search-namespace) + +## Workers VPC + +The new `VpcService` resource connects Workers to private network services through a Cloudflare Tunnel. Define the tunnel, the service, and the Worker in one file: + +```ts +import { Tunnel, VpcService, Worker } from "alchemy/cloudflare"; + +const tunnel = await Tunnel("api-tunnel", { + ingress: [{ service: "http://internal-api:8080" }], +}); + +const vpcService = await VpcService("private-api", { + httpPort: 8080, + host: { + hostname: "internal-api", + resolverNetwork: { + tunnel, + resolverIps: ["10.0.0.1"], + }, + }, +}); + +const worker = await Worker("api-gateway", { + entrypoint: "./src/worker.ts", + bindings: { + PRIVATE_API: vpcService, + }, +}); +``` + +Inside the Worker the binding is a plain `Fetcher` — `env.PRIVATE_API.fetch(...)` routes the request to the private host over the tunnel. Hosts can be a hostname (resolved over the tunnel), an IPv4 or IPv6 address, or dual-stack. + +See the [VpcService docs](/providers/cloudflare/vpc-service) for the full set of host configurations. + +## Raise the subrequest limit + +Workers on paid plans can now raise the per-invocation subrequest cap, alongside the existing `cpu_ms` limit: + +```ts +await Worker("scraper", { + entrypoint: "./src/scraper.ts", + limits: { + subrequests: 100_000, // paid plans support up to 10,000,000 + }, +}); +``` + +## Also in this release + +- **PlanetScale `DefaultRole`.** New resource that returns the default credentials for a PlanetScale Postgres branch — hand `role.connectionUrl` straight to Hyperdrive. The same PR added missing `Database` props and fixed an erroneous branch promotion. See the [DefaultRole docs](/providers/planetscale/default-role). +- **PlanetScale respects `scope.adopt`.** Databases now honor the scope-level adopt option instead of requiring `adopt: true` per resource. +- **Explicit `accountId` with OAuth.** Cloudflare resources now respect an explicitly configured account ID when authenticating with OAuth credentials instead of always using the token's default account. +- **Fixes.** Request abort signals now propagate to the Miniflare worker proxy in `alchemy dev`; deleting an already-deleted D1 database no longer fails; R2 custom domains no longer trigger erroneous replacement; the Vite plugin skips config-file validation during `svelte-kit sync` and `svelte-check`; Docker container start/update handling is more robust; and the SQLite state store sets a 5s `busy_timeout` to avoid lock errors under concurrency. + +## Thanks + +Thanks to **Gabriel Massadas**, **Jan Henning**, **John Royal**, **Benjamin Kraatz**, **Julien Roubieu**, **utopy**, and **yeoularu** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.84.0...v0.85.2) for everything in the 0.85 line. diff --git a/alchemy-web/src/content/docs/blog/2026-02-24-alchemy-0-86.md b/alchemy-web/src/content/docs/blog/2026-02-24-alchemy-0-86.md new file mode 100644 index 0000000000..677a4b852c --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-02-24-alchemy-0-86.md @@ -0,0 +1,45 @@ +--- +title: "Alchemy 0.86: VpcServiceRef" +date: 2026-02-24 +authors: Sam Goodwin +excerpt: A small release with one addition — VpcServiceRef lets you bind a Worker to an existing VPC service by name or ID without taking over its lifecycle. +--- + +A small release with one addition: `VpcServiceRef`. + +0.85 introduced the [VpcService](/providers/cloudflare/vpc-service) resource for connecting Workers to private networks over a Cloudflare Tunnel. But in a lot of teams the VPC service already exists — created by another stack, another team, or by hand in the dashboard — and you just want to bind to it, not manage it. + + + +## Reference an existing VPC service + +`VpcServiceRef` looks up an existing VPC service by name or by ID and returns it in the same shape as `VpcService`, so it plugs into Worker bindings directly: + +```ts +import { VpcServiceRef, Worker } from "alchemy/cloudflare"; + +const vpcService = await VpcServiceRef({ + name: "my-vpc-service", +}); + +const worker = await Worker("api-gateway", { + entrypoint: "./src/worker.ts", + bindings: { + PRIVATE_API: vpcService, + }, +}); +``` + +If you have the Cloudflare-assigned ID, use that instead: + +```ts +const vpcService = await VpcServiceRef({ + serviceId: "123e4567-e89b-12d3-a456-426614174000", +}); +``` + +Unlike `VpcService`, this is a pure lookup — nothing is written to state, and destroying your app never touches the underlying service. It's the same pattern Alchemy uses elsewhere for referencing infrastructure that lives outside your app. + +## Thanks + +Thanks to **John Royal** for this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.85.2...v0.86.0). diff --git a/alchemy-web/src/content/docs/blog/2026-02-25-alchemy-0-87.md b/alchemy-web/src/content/docs/blog/2026-02-25-alchemy-0-87.md new file mode 100644 index 0000000000..0b2bf69370 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-02-25-alchemy-0-87.md @@ -0,0 +1,40 @@ +--- +title: "Alchemy 0.87: Container.inspect()" +date: 2026-02-25 +authors: Sam Goodwin +excerpt: Docker containers now expose an inspect() method for reading runtime state — most usefully, the actual host port when you let Docker pick one. Plus secrets in container environments. +--- + +A small release focused on the Docker provider: `Container` now exposes an `inspect()` method for reading runtime state, and container environment variables accept Alchemy secrets. + + + +## Inspect a running container + +The `Container` resource now returns an `inspect()` method that queries Docker for the container's live runtime info. The headline use case is randomized host ports: publish a port with `external: 0`, let Docker pick a free one, then read back what it chose: + +```ts +import { Container } from "alchemy/docker"; + +const db = await Container("postgres", { + image: "postgres:17", + ports: [{ external: 0, internal: 5432 }], // let Docker pick a free host port + start: true, +}); + +const info = await db.inspect(); +const hostPort = info.ports["5432/tcp"]; + +console.log(`postgres is listening on localhost:${hostPort}`); +``` + +`inspect()` returns the port map keyed by `"{internalPort}/{protocol}"`, resolved from the running container — no more hardcoding host ports or fighting over who owns 5432 across stages and CI runs. + +## Also in this release + +- **Secrets in container environments.** `Container`'s `environment` prop now accepts `Secret` values alongside plain strings, so credentials flow through `alchemy.secret(...)` and stay encrypted in state. See the [Container docs](/providers/docker/container). +- **Fixes.** Updating a Cloudflare DNS record now handles multiple existing records with the same name instead of erroring. + +## Thanks + +Thanks to **Alec Larson** and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.86.0...v0.87.0). diff --git a/alchemy-web/src/content/docs/blog/2026-03-11-alchemy-0-88.md b/alchemy-web/src/content/docs/blog/2026-03-11-alchemy-0-88.md new file mode 100644 index 0000000000..2e6891aeca --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-03-11-alchemy-0-88.md @@ -0,0 +1,97 @@ +--- +title: "Alchemy 0.88: R2 Event Notifications" +date: 2026-03-11 +authors: Sam Goodwin +excerpt: R2 buckets can now push object-create and object-delete events to a Queue with the new R2BucketNotification resource. PlanetScale Postgres databases gain declarative extensions, and Workflows get a configurable steps limit. +--- + +Alchemy 0.88 adds `R2BucketNotification` — wire an R2 bucket to a Cloudflare Queue and react to object uploads and deletes from a Worker. PlanetScale Postgres databases can now declare their extensions (pgvector, pg_cron, and friends) in code, and Cloudflare Workflows get a configurable steps limit. + + + +## R2 event notifications + +The new `R2BucketNotification` resource configures [R2 event notifications](https://developers.cloudflare.com/r2/buckets/event-notifications/): when objects are created or deleted in a bucket, R2 sends a message to a Queue. Combined with a queue consumer Worker, that's a complete upload-processing pipeline in one file: + +```ts +import alchemy from "alchemy"; +import { + Queue, + R2Bucket, + R2BucketNotification, + Worker, + type R2BucketNotificationMessage, +} from "alchemy/cloudflare"; + +const app = await alchemy("uploads"); + +const bucket = await R2Bucket("uploads"); +const queue = await Queue("upload-events"); + +await R2BucketNotification("upload-notifications", { + bucket, + queue, + eventTypes: ["object-create"], + prefix: "incoming/", + suffix: ".pdf", +}); + +export const worker = await Worker("processor", { + entrypoint: "./src/processor.ts", + bindings: { BUCKET: bucket }, + eventSources: [queue], +}); + +await app.finalize(); +``` + +Typing the Queue as `Queue` gives your consumer a fully typed payload — bucket, object key, size, eTag, and event time. `prefix` and `suffix` filters narrow which objects trigger events, and either one (but not both) can be an array to fan out into one rule per pattern — for example, `suffix: [".mp3", ".wav", ".flac"]`. + +See the [R2BucketNotification docs](/providers/cloudflare/r2-bucket-notification) for all the shapes. + +## PlanetScale Postgres extensions + +PlanetScale Postgres databases can now declare extensions directly on the `Database` resource. Present keys are enabled (with config or defaults), absent keys are disabled — the resource diffs and syncs on every deploy: + +```ts +import { Database } from "alchemy/planetscale"; + +const db = await Database("my-pg-db", { + kind: "postgresql", + clusterSize: "PS_10", + extensions: { + vector: { hnswEfSearch: 100 }, + pgCron: {}, + pgStatStatements: { max: 10000 }, + }, +}); +``` + +No more clicking through the dashboard to turn on pgvector before your first deploy — the extension state lives with the rest of your infrastructure. + +## Workflow step limits + +Cloudflare Workflows on paid plans default to 10,000 steps per instance but can go up to 25,000. That's now configurable on the `Workflow` binding: + +```ts +import { Worker, Workflow } from "alchemy/cloudflare"; + +await Worker("processor", { + entrypoint: "./src/worker.ts", + bindings: { + ORDERS: Workflow("orders", { + className: "OrderWorkflow", + limits: { steps: 25_000 }, + }), + }, +}); +``` + +## Also in this release + +- **Existing Docker networks are adopted gracefully.** Creating a `Network` whose name already exists in Docker now inspects and reuses it instead of failing. +- **PlanetScale adoption validates immutable props.** Adopting a database with `adopt: true` now checks that `kind`, `region`, and `arch` match the live database and fails fast with a clear error instead of silently diverging. + +## Thanks + +Thanks to **Alec Larson**, **Jan Henning**, **Michael K**, and **Misha Kaletsky** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.87.0...v0.88.0). diff --git a/alchemy-web/src/content/docs/blog/2026-03-20-alchemy-0-90.md b/alchemy-web/src/content/docs/blog/2026-03-20-alchemy-0-90.md new file mode 100644 index 0000000000..a8cd346250 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-03-20-alchemy-0-90.md @@ -0,0 +1,50 @@ +--- +title: "Alchemy 0.90: Keep DNS Records When You Destroy" +date: 2026-03-20 +authors: Sam Goodwin +excerpt: DnsRecords now supports delete: false, so critical records like MX and SPF survive a destroy or a refactor. Plus fixes for queue consumers in local dev and a Miniflare container regression. +--- + +DNS is the part of your infrastructure you least want an IaC tool to delete by accident. A stray `destroy` that takes out your MX records is a bad afternoon. Alchemy 0.90 adds `delete: false` to the `DnsRecords` resource, so you can manage records in code while guaranteeing they stay in the zone no matter what happens to your Alchemy app. + + + +## `delete: false` for DnsRecords + +Set `delete: false` and Alchemy will create and update records as usual, but never remove them from Cloudflare — not on `destroy`, and not when a record is dropped from the `records` array: + +```ts +import { DnsRecords } from "alchemy/cloudflare"; + +const records = await DnsRecords("critical-dns", { + zoneId: "YOUR_ZONE_ID", + delete: false, + records: [ + { + name: "example.com", + type: "MX", + content: "aspmx.l.google.com", + priority: 1, + }, + { + name: "example.com", + type: "TXT", + content: "v=spf1 include:_spf.google.com ~all", + }, + ], +}); +``` + +When the resource is destroyed, it's removed from Alchemy's state and the records are left untouched in the zone. This follows the same convention as other data-holding resources like `R2Bucket` and `D1Database`, where deletion is opt-out. + +See the [DnsRecords docs](/providers/cloudflare/dns-records) for the full API. + +## Also in this release + +- **Queue consumers in local dev.** Queue consumers now use the queue's dev id for their resource ID in local dev mode, fixing consumer tracking when running `alchemy dev`. +- **Miniflare pin.** Pinned the Miniflare version to work around a container regression in local dev. +- **workers.dev with remote dev (0.90.1).** The workers.dev subdomain is now enabled correctly when a Worker uses `dev: { remote: true }`. + +## Thanks + +Thanks to **Gareth Andrew**, **Yanqi Zong**, and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.89.0...v0.90.1). diff --git a/alchemy-web/src/content/docs/blog/2026-04-09-alchemy-0-91.md b/alchemy-web/src/content/docs/blog/2026-04-09-alchemy-0-91.md new file mode 100644 index 0000000000..0d5a7999ae --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-04-09-alchemy-0-91.md @@ -0,0 +1,94 @@ +--- +title: "Alchemy 0.91: Typed RPC for Websites and wasm from node_modules" +date: 2026-04-09 +authors: Sam Goodwin +excerpt: Website resources like Vite and Astro now carry your RPC entrypoint types, Workers can bundle .wasm files straight out of node_modules, and Worker gains a delete prop for keeping scripts around after removal. +--- + +Alchemy 0.91 is about making Workers feel more like regular TypeScript. The `Website` family of resources (`Vite`, `Astro`, `SvelteKit`, `Nuxt`, `ReactRouter`, `TanStackStart`, and friends) now carries your RPC entrypoint types end-to-end, so calling a website's Worker over a service binding is fully typed. Workers can also bundle `.wasm` files imported from npm packages, and the `Worker` resource picks up a `delete` prop for scripts you never want Alchemy to remove. + + + +## Typed RPC entrypoints for Websites + +Workers have supported the `rpc` prop for a while: point it at your `WorkerEntrypoint` class and every service binding to that Worker gets typed RPC methods. As of 0.91, the same works for all `Website`-based resources. + +Say your Vite site's server entrypoint exposes RPC methods: + +```ts +// src/server.ts +import { WorkerEntrypoint } from "cloudflare:workers"; + +export default class API extends WorkerEntrypoint { + async getPost(slug: string): Promise<{ slug: string; title: string }> { + return { slug, title: "Hello" }; + } +} +``` + +Pass the type through with `rpc`, then bind the website into another Worker: + +```ts +// alchemy.run.ts +import alchemy, { type } from "alchemy"; +import { Vite, Worker } from "alchemy/cloudflare"; +import type API from "./src/server.ts"; + +const app = await alchemy("my-app"); + +const website = await Vite("website", { + entrypoint: "./dist/server.js", // built from ./src/server.ts + rpc: type, +}); + +export const cron = await Worker("cron", { + entrypoint: "./src/cron.ts", + bindings: { + SITE: website, + }, +}); + +await app.finalize(); +``` + +In the consuming Worker, `env.SITE.getPost("hello")` is now fully typed — argument types, return types, autocomplete, the lot. Previously this only worked with the raw `Worker` resource; now your framework app is a first-class RPC citizen too. See [Worker RPC docs](/providers/cloudflare/worker) for the underlying mechanics. + +## Bundle wasm from node_modules + +Workers already supported importing local `.wasm` files. 0.91 extends that to bare specifiers, so wasm shipped inside npm packages just works: + +```ts +// src/worker.ts +import { Resvg, initWasm } from "@resvg/resvg-wasm"; +import wasmModule from "@resvg/resvg-wasm/index_bg.wasm"; + +await initWasm(wasmModule); +``` + +Relative imports resolve like before; bare specifiers now go through Node module resolution to find the file in `node_modules`. Alchemy's bundler copies the `.wasm` file into the Worker bundle and rewrites the import to a stable module specifier — no build plugins, no manual copying. + +## `delete: false` for Workers + +`Worker` now accepts a `delete` prop. Set it to `false` and removing the Worker from your Alchemy app removes it from state but leaves the deployed script running: + +```ts +const worker = await Worker("critical-api", { + name: "critical-api", + entrypoint: "./src/api.ts", + delete: false, +}); +``` + +The flag flows through to the resources the Worker manages on your behalf — routes, custom domains, queue consumers, and containers — so nothing attached to the script is torn down either. + +:::caution[Breaking change in 0.91.0, reverted in 0.91.1] +0.91.0 changed the default Workflow name from the class name (or id) to a physical name scoped with your app and stage (`${app}-${id}-${stage}`). This was reverted in 0.91.1, restoring the old defaults. If you deployed on exactly 0.91.0 and your workflow names changed, upgrading to 0.91.1 or later brings the previous names back — and if you want names that never depend on defaults, set `workflowName` explicitly on your [Workflow](/providers/cloudflare/workflow). +::: + +## Also in this release + +- **Queue handler cleanup.** Deploys no longer fail with Cloudflare error 11001 ("queue handler missing") after you remove a Worker's `queue` handler and its event sources — stale queue consumers are now removed automatically before the script is updated. + +## Thanks + +Thanks to **Michael K** and **John Royal** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.90.1...v0.91.2). diff --git a/alchemy-web/src/content/docs/blog/2026-04-20-alchemy-0-92.md b/alchemy-web/src/content/docs/blog/2026-04-20-alchemy-0-92.md new file mode 100644 index 0000000000..bf33e22ab7 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-04-20-alchemy-0-92.md @@ -0,0 +1,81 @@ +--- +title: "Alchemy 0.92: Send Email from Workers" +date: 2026-04-20 +authors: Sam Goodwin +excerpt: Cloudflare Email Service's send_email binding is now first-class in Alchemy. Add EmailSender() to a Worker's bindings and send transactional email from env — with local dev simulation and an opt-in remote mode. +--- + +Cloudflare's [Email Service](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) lets a Worker send outbound email through a native `send_email` binding — no third-party API keys, no SMTP. Alchemy 0.92 makes it a first-class binding: add `EmailSender()` to a Worker's `bindings` and call `env.EMAIL.send()` from your code. + + + +## The `EmailSender` binding + +```ts +// alchemy.run.ts +import alchemy from "alchemy"; +import { EmailSender, Worker } from "alchemy/cloudflare"; + +const app = await alchemy("cloudflare-email"); + +export const worker = await Worker("email-worker", { + entrypoint: "./worker.ts", + compatibility: "node", + bindings: { + EMAIL: EmailSender({ + allowedSenderAddresses: ["noreply@example.com"], + }), + }, +}); + +await app.finalize(); +``` + +Then send from the Worker — plain text, or HTML with a text fallback: + +```ts +// worker.ts +interface Env { + EMAIL: SendEmail; +} + +export default { + async fetch(_request: Request, env: Env) { + await env.EMAIL.send({ + from: "noreply@example.com", + to: "hello@example.com", + subject: "Welcome", + html: "

Welcome

Your account is ready.

", + text: "Welcome. Your account is ready.", + }); + + return new Response("sent"); + }, +}; +``` + +The binding supports the guardrails from the Cloudflare API: restrict who can send with `allowedSenderAddresses`, and restrict where mail can go with either a single `destinationAddress` or a list of `allowedDestinationAddresses`. + +It also works in `alchemy dev`. By default, sends are simulated locally and the messages are logged so you can see exactly what would have gone out. If you want to send real email while developing, opt in with `dev`: + +```ts +EmailSender({ + allowedSenderAddresses: ["noreply@example.com"], + dev: { remote: true }, +}); +``` + +:::note +Cloudflare Email Service is in beta and available on the Workers Paid plan. +::: + +See the [EmailSender docs](/providers/cloudflare/email-sender) for the full API, including how it composes with Alchemy's existing Email Routing resources. + +## Also in this release + +- **Cloudflare login fix.** `alchemy login` no longer requests the retired Agents Gateway (`agw`) OAuth scopes, which were breaking the Cloudflare authorization flow. +- **Miniflare rollback (0.92.2).** Reverted a Miniflare version bump that caused problems in local dev. + +## Thanks + +Thanks to **Dawson**, **John Royal**, and **Matt** for contributing to this release. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.91.2...v0.92.2). diff --git a/alchemy-web/src/content/docs/blog/2026-04-24-alchemy-0-93.md b/alchemy-web/src/content/docs/blog/2026-04-24-alchemy-0-93.md new file mode 100644 index 0000000000..80f80f5b84 --- /dev/null +++ b/alchemy-web/src/content/docs/blog/2026-04-24-alchemy-0-93.md @@ -0,0 +1,120 @@ +--- +title: "Alchemy 0.93: Zero Trust Access, alchemy state, and AI Search Namespaces" +date: 2026-04-24 +authors: Sam Goodwin +excerpt: Cloudflare Zero Trust Access is now fully modeled in Alchemy — applications, policies, groups, identity providers, and service tokens. Plus new alchemy state CLI commands for inspecting your state store, and AI Search namespaces with dynamic Worker bindings. +--- + +The 0.93 line is a big one. The headline is Cloudflare Zero Trust Access: the auth layer in front of your Workers, tunnels, and internal tools can now live in the same `alchemy.run.ts` as everything else. Alongside it, the CLI gains `alchemy state` commands for inspecting exactly what Alchemy is tracking, and AI Search gets namespaces with a dynamic Worker binding for managing search instances at runtime. + + + +## Cloudflare Zero Trust Access + +Five new resources cover the Access model end-to-end: [AccessApplication](/providers/cloudflare/access-application), [AccessPolicy](/providers/cloudflare/access-policy), [AccessGroup](/providers/cloudflare/access-group), [AccessIdentityProvider](/providers/cloudflare/access-identity-provider), and [AccessServiceToken](/providers/cloudflare/access-service-token). + +Protecting an internal hostname is a policy plus an application: + +```ts +import { AccessApplication, AccessPolicy } from "alchemy/cloudflare"; + +const employees = await AccessPolicy("employees", { + name: "Employees", + decision: "allow", + include: [{ email_domain: { domain: "acme.com" } }], +}); + +const admin = await AccessApplication("admin", { + type: "self_hosted", + name: "Internal Admin", + domain: "admin.acme.com", + policies: [employees], + sessionDuration: "8h", +}); +``` + +Policies are reusable — attach the same one to as many applications as you like. Rule references are resource-aware, so a rule like `{ group: { id: adminGroup } }` accepts either a raw ID or the Alchemy resource itself. + +For machine-to-machine access (CI runners, cross-service calls), mint a service token and hand its credentials to whatever needs in: + +```ts +import { AccessServiceToken, Worker } from "alchemy/cloudflare"; + +const token = await AccessServiceToken("worker-to-admin", { + name: "worker-to-admin", +}); + +await Worker("admin-client", { + entrypoint: "./src/worker.ts", + bindings: { + CF_ACCESS_CLIENT_ID: token.clientId, + CF_ACCESS_CLIENT_SECRET: token.clientSecret!, + }, +}); +``` + +`AccessApplication` also supports `bookmark` tiles for the Access launcher and `saas` integrations where Cloudflare brokers SSO to a vendor over OIDC or SAML. `AccessIdentityProvider` wires up GitHub, Okta, Azure AD, Google, and the rest, so the entire login story is code. + +## Inspect your state with `alchemy state` + +Alchemy's state store has always been pluggable — local files, D1, R2, your own backend. New in 0.93, the CLI can show you what's in it: + +```sh +alchemy state tree # print all stages, scopes, and resources as a tree +alchemy state list # list fully-qualified resource names, one per line +alchemy state get my-app/prod/website # print the JSON state for one resource +``` + +`tree` gives you the shape of your app at a glance; `list` is grep-friendly; `get` takes a fully-qualified name (`app/stage/.../id`) and dumps the stored state for that resource, which is the fastest way to answer "what does Alchemy think this Worker looks like?" without opening the state files by hand. All three run against whatever state store your app configures — your resources are never created or modified. + +## AI Search namespaces + +0.93 rounds out the AI Search story that started with the `AiSearch` resource. The new [AiSearchNamespace](/providers/cloudflare/ai-search-namespace) resource groups AI Search instances together, and — the interesting part — binds to Workers as a dynamic namespace, so your Worker can create, search, and delete instances at runtime: + +```ts +import { AiSearchNamespace, Worker } from "alchemy/cloudflare"; + +const docs = await AiSearchNamespace("docs", { + name: "docs", +}); + +await Worker("api", { + entrypoint: "./src/worker.ts", + bindings: { + DOCS: docs, + }, +}); +``` + +```ts +// src/worker.ts +export default { + async fetch(request: Request, env: Env) { + // Query an existing instance in the namespace + const results = await env.DOCS.get("my-blog").search({ + query: "How does caching work?", + }); + + // Create a new instance at runtime — no redeploy + const tenant = await env.DOCS.create({ id: "tenant-123" }); + await tenant.items.upload("faq.md", "# FAQ\n\n..."); + + return Response.json(results); + }, +}; +``` + +That per-tenant pattern — one search instance per customer, provisioned on demand from inside the Worker — is the reason namespaces exist. Individual [AiSearch](/providers/cloudflare/ai-search) instances can also be bound directly as standalone `ai_search` bindings with typed `search`, `chat`, `items`, and `jobs` APIs. The new [AI Search guide](/guides/cloudflare-ai-search) walks through the whole setup. + +## Also in this release + +- **Local Explorer.** `alchemy dev` now enables Miniflare's Local Explorer, a built-in UI at `/cdn-cgi/explorer` on your dev Worker for inspecting local resources. +- **`overrideExistingOrigin` on CustomDomain.** Opt in to forcibly transfer a hostname that's currently bound to a different Worker, instead of failing with a 409. Destructive to the previous binding, so it's off by default. See [CustomDomain](/providers/cloudflare/custom-domain). +- **Container placement constraints.** `Container` now accepts `constraints: { regions, jurisdiction }` to control where instances run — e.g. `{ regions: ["ENAM", "WNAM"], jurisdiction: "fedramp" }`. See [Container](/providers/cloudflare/container). +- **Dev tunnels.** `dev.tunnel` now works through the Vite/Website dev-server spawn path, `trycloudflare.com` hosts are auto-allowed in Vite, and the original request URL is preserved through the tunnel. +- **Containers.** Prebuilt images no longer trigger an unnecessary Docker pull, and registry credentials resolve lazily instead of at import time. +- **Fixes.** Miniflare and Wrangler were upgraded and moved to regular dependencies; the remote-bindings proxy was updated for Cloudflare's new edge preview API; R2 object operations forward the jurisdiction header and `R2Bucket` defers creating its API client until it's actually needed outside local mode; `BrowserRendering` binds to the correct `BrowserRun` runtime type; Stripe `Price` decimal fields are coerced to strings; and `CustomDomain` handles the "domain exists" error on create. + +## Thanks + +Thanks to **Gabriel Massadas**, **Alex Dunne**, **Xin Chen**, **Yan**, **Christian Klotz**, **John Royal**, **Jordan**, **Julien Roubieu**, **Michael K**, and **Amp** for contributing to this release line. See the [full changelog](https://github.com/alchemy-run/alchemy/compare/v0.92.2...v0.93.12).