| 1 | --- |
| 2 | title: 2.0.0-beta.36 - Images & Hyperdrive Bindings |
| 3 | date: 2026-05-09 |
| 4 | category: release |
| 5 | excerpt: Cloudflare Images binding, Hyperdrive as a Worker binding, R2 object lifecycle rules, and a dev-mode networking fix. |
| 6 | --- |
| 7 | |
| 8 | `v2.0.0-beta.36` is mostly a Cloudflare-themed release — |
| 9 | three new Worker bindings (Images, Hyperdrive, |
| 10 | R2 lifecycle policies), all from outside the core team. |
| 11 | Props to the contributors throughout, and full credits in |
| 12 | the [Contributors section](#contributors). |
| 13 | |
| 14 | ## Cloudflare Images binding |
| 15 | |
| 16 | Cloudflare's image transformation runtime, exposed as a |
| 17 | Worker binding. The Effect-native client takes an Effect |
| 18 | `Stream<Uint8Array>` input — typically the request body — |
| 19 | and either streams a transformed image back out or returns |
| 20 | typed metadata. |
| 21 | |
| 22 | ```typescript |
| 23 | // alchemy.run.ts — declare the pipeline resource |
| 24 | export const Pipeline = yield* Cloudflare.Images.Images({ name: "PIPELINE" }); |
| 25 | |
| 26 | // in your Worker |
| 27 | export default class ImageWorker extends Cloudflare.Worker<ImageWorker>()( |
| 28 | "ImageWorker", |
| 29 | { main: import.meta.filename }, |
| 30 | Effect.gen(function* () { |
| 31 | const images = yield* Cloudflare.Images.bind(Pipeline); |
| 32 | |
| 33 | return { |
| 34 | fetch: Effect.gen(function* () { |
| 35 | const request = yield* HttpServerRequest; |
| 36 | |
| 37 | // Probe the upload — returns format, width, height, etc. |
| 38 | if (request.url.endsWith("/info")) { |
| 39 | const info = yield* images.info(request.stream); |
| 40 | return yield* HttpServerResponse.json(info); |
| 41 | } |
| 42 | |
| 43 | // Transform: resize to 512×512 WebP, stream the result back. |
| 44 | const transformed = yield* images |
| 45 | .input(request.stream) |
| 46 | .transform({ width: 512, height: 512 }) |
| 47 | .output({ format: "image/webp" }); |
| 48 | return HttpServerResponse.stream(transformed.body); |
| 49 | }), |
| 50 | }; |
| 51 | }).pipe(Effect.provide(Cloudflare.ImagesBindingLive)), |
| 52 | ) {} |
| 53 | ``` |
| 54 | |
| 55 | — *Thanks to **Alex** ([#237](https://github.com/alchemy-run/alchemy/pull/237)) |
| 56 | for the contribution.* |
| 57 | [`Images.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Images/Images.ts) |
| 58 | |
| 59 | ## Hyperdrive in Worker bindings |
| 60 | |
| 61 | `Hyperdrive` resources are bindable on Workers the same way |
| 62 | as R2, KV, D1. Two flavors to know: |
| 63 | |
| 64 | **Declare a Hyperdrive over an existing origin string.** |
| 65 | Useful when the database lives outside of Alchemy (existing |
| 66 | RDS, Supabase, etc.): |
| 67 | |
| 68 | ```typescript |
| 69 | const Hyperdrive = yield* Cloudflare.Hyperdrive("DbPool", { |
| 70 | origin: { connectionString: process.env.POSTGRES_URL! }, |
| 71 | }); |
| 72 | ``` |
| 73 | |
| 74 | **Wire it onto a Neon branch you already deployed.** |
| 75 | `Neon.Branch` exposes a pre-parsed `origin` output that |
| 76 | `Cloudflare.Hyperdrive` takes directly — Alchemy orders the |
| 77 | deploy graph correctly: |
| 78 | |
| 79 | ```typescript |
| 80 | // src/Db.ts |
| 81 | export const NeonDb = Effect.gen(function* () { |
| 82 | const project = yield* Neon.Project("app-db", { region: "aws-us-east-1" }); |
| 83 | const branch = yield* Neon.Branch("app-branch", { project }); |
| 84 | return { project, branch }; |
| 85 | }); |
| 86 | |
| 87 | export const Hyperdrive = Effect.gen(function* () { |
| 88 | const { branch } = yield* NeonDb; |
| 89 | return yield* Cloudflare.Hyperdrive("app-hyperdrive", { |
| 90 | origin: branch.origin, // direct (non-pooled) Neon endpoint |
| 91 | }); |
| 92 | }); |
| 93 | ``` |
| 94 | |
| 95 | Bind it on the Worker and read the pooled connection |
| 96 | string at runtime — Hyperdrive does the connection pooling |
| 97 | so the Worker can spin up a fresh `pg` client per request |
| 98 | without exhausting the database: |
| 99 | |
| 100 | ```typescript |
| 101 | export default class Api extends Cloudflare.Worker<Api>()( |
| 102 | "Api", |
| 103 | { |
| 104 | main: import.meta.filename, |
| 105 | compatibility: { flags: ["nodejs_compat"] }, |
| 106 | }, |
| 107 | Effect.gen(function* () { |
| 108 | const hd = yield* Cloudflare.Hyperdrive.bind(Hyperdrive); |
| 109 | |
| 110 | return { |
| 111 | fetch: Effect.gen(function* () { |
| 112 | const connectionString = yield* hd.connectionString; |
| 113 | const rows = yield* Effect.promise(async () => { |
| 114 | const client = new Client({ connectionString }); |
| 115 | await client.connect(); |
| 116 | try { |
| 117 | const r = await client.query("SELECT now() as now"); |
| 118 | return r.rows; |
| 119 | } finally { |
| 120 | await client.end().catch(() => {}); |
| 121 | } |
| 122 | }); |
| 123 | return yield* HttpServerResponse.json({ rows }); |
| 124 | }), |
| 125 | }; |
| 126 | }).pipe(Effect.provide(Cloudflare.HyperdriveConnectionLive)), |
| 127 | ) {} |
| 128 | ``` |
| 129 | |
| 130 | — *Thanks to **Baptiste Arnaud** |
| 131 | ([#282](https://github.com/alchemy-run/alchemy/pull/282)) |
| 132 | for the contribution.* |
| 133 | [Tutorial › Hyperdrive](/cloudflare/data/hyperdrive) |
| 134 | |
| 135 | ## R2 lifecycle rules |
| 136 | |
| 137 | Object lifecycle policies on `Bucket` — age-based |
| 138 | deletion, storage-class transitions, multipart-upload |
| 139 | abort — declared inline alongside the rest of the bucket |
| 140 | config. Max 1000 rules per bucket; pass an empty array (or |
| 141 | omit) to clear all rules. |
| 142 | |
| 143 | ```typescript |
| 144 | const Logs = yield* Cloudflare.R2.Bucket("Logs", { |
| 145 | lifecycleRules: [ |
| 146 | { |
| 147 | id: "archive-then-delete", |
| 148 | prefix: "logs/", |
| 149 | // After 60 days: move to Infrequent Access. |
| 150 | storageClassTransitions: [ |
| 151 | { |
| 152 | condition: { type: "Age", maxAge: 60 * 60 * 24 * 60 }, |
| 153 | storageClass: "InfrequentAccess", |
| 154 | }, |
| 155 | ], |
| 156 | // After 365 days: delete entirely. |
| 157 | deleteObjectsTransition: { |
| 158 | condition: { type: "Age", maxAge: 60 * 60 * 24 * 365 }, |
| 159 | }, |
| 160 | // Stale multipart uploads — drop after 7 days. |
| 161 | abortMultipartUploadsTransition: { |
| 162 | condition: { type: "Age", maxAge: 60 * 60 * 24 * 7 }, |
| 163 | }, |
| 164 | }, |
| 165 | ], |
| 166 | }); |
| 167 | ``` |
| 168 | |
| 169 | Each entry is reconciled in place — edit the array and the |
| 170 | next deploy applies the delta against R2's live policy. |
| 171 | |
| 172 | — *Thanks to **Baptiste Arnaud** |
| 173 | ([#284](https://github.com/alchemy-run/alchemy/pull/284)) |
| 174 | for the contribution.* See [Cloudflare R2 docs › Object |
| 175 | lifecycles](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) |
| 176 | for the underlying behavior. |
| 177 | |
| 178 | ## Fixes worth knowing about |
| 179 | |
| 180 | - **`*.localhost` resolution in dev mode.** `bun alchemy |
| 181 | dev` now routes `*.localhost` hostnames through a |
| 182 | custom undici dispatcher, so cross-Worker `fetch` calls |
| 183 | to e.g. `https://backend.localhost` resolve to the local |
| 184 | sidecar instead of failing DNS. |
| 185 | |
| 186 | ## Performance |
| 187 | |
| 188 | - **Smoke tests install canaries at the workspace root** |
| 189 | rather than per-fixture. Batched smoke runs are |
| 190 | noticeably faster on cold caches. |
| 191 | |
| 192 | ## Contributors |
| 193 | |
| 194 | Big thank-you to everyone who shipped code in this beta: |
| 195 | |
| 196 | - **Alex** — Cloudflare Images binding ([#237](https://github.com/alchemy-run/alchemy/pull/237)) |
| 197 | - **Baptiste Arnaud** — Hyperdrive in Worker bindings ([#282](https://github.com/alchemy-run/alchemy/pull/282)) |
| 198 | - **Baptiste Arnaud** — R2 lifecycle rules ([#284](https://github.com/alchemy-run/alchemy/pull/284)) |
| 199 | |
| 200 | ## Where to go next |
| 201 | |
| 202 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta36) |
| 203 | - [Compare v2.0.0-beta.35 → v2.0.0-beta.36](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.35...v2.0.0-beta.36) |