| 1 | --- |
| 2 | title: 2.0.0-beta.78 - Git Hosting, Stripe & Worker Previews |
| 3 | date: 2026-09-17T09:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.78 ships alchemy/Git, an embeddable, pluggable git server you mount in your own Effect HTTP API, plus a Stripe provider, Worker Previews, ten new GitHub resources, an alchemy dev that uses half the memory, and a round of drift and default-handling fixes across RDS, S3, and security groups. |
| 6 | --- |
| 7 | |
| 8 | beta.78 ships **`alchemy/Git`**, an embeddable, pluggable git server |
| 9 | built from Effect Layers. Mount it inside any Effect HTTP API and |
| 10 | pick where refs and bytes live — Durable Objects and R2 on |
| 11 | Cloudflare, S3 and Lambda on AWS, or a store of your own. A new |
| 12 | **Stripe** provider provisions Stripe resources and binds them to |
| 13 | your Workers and Functions. **Worker Previews** give every branch its |
| 14 | own URL and isolated Durable Objects. The GitHub provider gains ten |
| 15 | resources, `alchemy dev` runs on half the memory, and a round of |
| 16 | fixes to RDS, S3, and security groups repairs drift and restores |
| 17 | defaults when props are removed. |
| 18 | |
| 19 | :::caution |
| 20 | **Breaking changes** |
| 21 | |
| 22 | - **effect `>=4.0.0-rc.115` is the new minimum peer** |
| 23 | ([#1562](https://github.com/alchemy-run/alchemy/pull/1562), |
| 24 | [#1601](https://github.com/alchemy-run/alchemy/pull/1601)). rc.113 |
| 25 | renamed the `Config` and `Flag` constructors to PascalCase — see |
| 26 | [below](#effect-rc115). |
| 27 | - **`SecurityGroup` `egress: []` now means no outbound traffic** |
| 28 | ([#1623](https://github.com/alchemy-run/alchemy/pull/1623)). It |
| 29 | used to fall back to AWS's allow-all default, the same as omitting |
| 30 | `egress`. Omit the prop to keep the default. Rules that exist in |
| 31 | AWS but not in your program are now removed on the next deploy |
| 32 | ([#1592](https://github.com/alchemy-run/alchemy/pull/1592), |
| 33 | [#1664](https://github.com/alchemy-run/alchemy/pull/1664)) — |
| 34 | `SecurityGroupRule` resources in the same stack are preserved. |
| 35 | - **RDS parameter groups reset undeclared overrides** |
| 36 | ([#1589](https://github.com/alchemy-run/alchemy/pull/1589)). An |
| 37 | omitted `parameters` is `{}`: values edited in the console converge |
| 38 | back to engine defaults. |
| 39 | - **`Alchemist` leaves the root barrel** |
| 40 | ([#1614](https://github.com/alchemy-run/alchemy/pull/1614)) so that |
| 41 | importing `alchemy` no longer loads every cloud: |
| 42 | |
| 43 | ```diff lang="typescript" |
| 44 | -import * as Alchemy from "alchemy"; |
| 45 | -Alchemy.Alchemist |
| 46 | +import * as Alchemist from "alchemy/Alchemist"; |
| 47 | |
| 48 | -import { S3BucketEventSource } from "alchemy/Server"; |
| 49 | +import { S3BucketEventSource } from "alchemy/Server/S3BucketEventSource"; |
| 50 | ``` |
| 51 | |
| 52 | - **`Prisma.Database` branch props are no longer nullable** |
| 53 | ([#1582](https://github.com/alchemy-run/alchemy/pull/1582)) — |
| 54 | Prisma's API now attaches every database to a Branch. Omit both |
| 55 | `branchId` and `branchGitName` to use the project default. |
| 56 | - **`Test.make` defaults to stage `test_$USER`** |
| 57 | ([#1579](https://github.com/alchemy-run/alchemy/pull/1579)), |
| 58 | matching `live_$USER` / `dev_$USER`. Pass `stage` to keep `test`. |
| 59 | ::: |
| 60 | |
| 61 | ## effect rc.115 |
| 62 | |
| 63 | alchemy now requires effect `>=4.0.0-rc.115` |
| 64 | ([#1562](https://github.com/alchemy-run/alchemy/pull/1562), |
| 65 | [#1601](https://github.com/alchemy-run/alchemy/pull/1601)): |
| 66 | |
| 67 | ```sh |
| 68 | bun add alchemy@latest effect@rc @effect/platform-bun@rc @effect/platform-node@rc |
| 69 | ``` |
| 70 | |
| 71 | rc.113 renamed the `Config` and `Flag` constructors to PascalCase: |
| 72 | |
| 73 | ```diff lang="typescript" |
| 74 | -Config.string("HOST") |
| 75 | +Config.String("HOST") |
| 76 | -Config.mapOrFail(...) |
| 77 | +Config.mapEffect(...) |
| 78 | -Flag.boolean("yes") |
| 79 | +Flag.Boolean("yes") |
| 80 | -Flag.choice("backend", …) |
| 81 | +Flag.Literals("backend", …) |
| 82 | ``` |
| 83 | |
| 84 | rc.113 also replaced `@effect/sql-pg`'s `pg` dependency with a native |
| 85 | wire client, which sent no SNI and rejected `sslmode=prefer`. Both |
| 86 | broke Aurora DSQL, Neon, and local Hyperdrive; alchemy's |
| 87 | `SQL.Postgres`, `Drizzle.Postgres`, and `postgresState` now derive the |
| 88 | TLS config from the URL |
| 89 | ([#1580](https://github.com/alchemy-run/alchemy/pull/1580), |
| 90 | [#1582](https://github.com/alchemy-run/alchemy/pull/1582)), and rc.115 |
| 91 | fixes the SNI half upstream |
| 92 | ([Effect-TS/effect#8174](https://github.com/Effect-TS/effect/pull/8174)). |
| 93 | |
| 94 | ## An embeddable, pluggable Git server |
| 95 | |
| 96 | `alchemy/Git` is a pluggable, embeddable git server |
| 97 | ([#1187](https://github.com/alchemy-run/alchemy/pull/1187), |
| 98 | [#1621](https://github.com/alchemy-run/alchemy/pull/1621)). It speaks |
| 99 | smart HTTP to any `git` client, serves a typed REST plane with pull |
| 100 | requests, and answers the GitHub REST v3 API so `gh api` and Octokit |
| 101 | work against it unchanged. Nothing ships a Worker — every part is an |
| 102 | Effect service with swappable Layers, assembled into a Worker you own: |
| 103 | |
| 104 | ```typescript |
| 105 | import * as Git from "alchemy/Git"; |
| 106 | import * as Http from "alchemy/Http"; |
| 107 | |
| 108 | export const GitObjects = Cloudflare.R2.Bucket("GitObjects"); |
| 109 | |
| 110 | const GitLive = Git.ApiLive.pipe( |
| 111 | Layer.provide(Git.ApiHandlersLive), // the engine's route handlers |
| 112 | Layer.provide(Authentication.layer), // yours: who may call what |
| 113 | Layer.provide(Git.ReposDurableObject), // refs, objects, pull requests |
| 114 | Layer.provide(Git.RegistryDurableObject), // owner/name → repo |
| 115 | Layer.provide(Git.HasherInline), // push verification |
| 116 | Layer.provide(Git.BlobStoreR2(GitObjects)), // packs, bundles, large pushes |
| 117 | ); |
| 118 | |
| 119 | export default Cloudflare.Worker( |
| 120 | "Git", |
| 121 | { main: import.meta.url, ...Git.GIT_WORKER_OPTIONS }, |
| 122 | Effect.gen(function* () { |
| 123 | const fetch = yield* HttpRouter.toHttpEffect( |
| 124 | GitLive.pipe(Layer.provide(Http.Platform)), |
| 125 | ); |
| 126 | return { fetch }; |
| 127 | }), |
| 128 | ); |
| 129 | ``` |
| 130 | |
| 131 | Each line is one decision with its own implementations. Change a |
| 132 | line and only that decision changes — here the bytes move to S3: |
| 133 | |
| 134 | ```diff lang="typescript" |
| 135 | -const GitObjects = Cloudflare.R2.Bucket("GitObjects"); |
| 136 | +const GitObjects = AWS.S3.Bucket("GitObjects"); |
| 137 | |
| 138 | const GitLive = Git.ApiLive.pipe( |
| 139 | Layer.provide(Git.ApiHandlersLive), |
| 140 | Layer.provide(Authentication.layer), |
| 141 | Layer.provide(Git.ReposDurableObject), |
| 142 | Layer.provide(Git.RegistryDurableObject), |
| 143 | Layer.provide(Git.HasherInline), |
| 144 | - Layer.provide(Git.BlobStoreR2(GitObjects)), |
| 145 | + Layer.provide(Git.BlobStoreS3(GitObjects)), |
| 146 | ); |
| 147 | ``` |
| 148 | |
| 149 | ### Embedded by design |
| 150 | |
| 151 | You build the HTTP server and mount `alchemy/Git`'s routes into it. |
| 152 | `Git.Api` is an Effect `HttpApi` group and `Git.Handlers` supplies |
| 153 | the default implementation of every route as a plain function. Add |
| 154 | the group to your own API and put your own `HttpApiMiddleware` in |
| 155 | front of it, and every request — `git clone` and `git push`, the REST |
| 156 | API, and the GitHub-compatible API `gh` uses — goes through your |
| 157 | authentication and your user management. The engine itself holds no |
| 158 | users, no credentials, and no policy. |
| 159 | |
| 160 | The middleware is native `HttpApiMiddleware`. Declare what it |
| 161 | provides, add the Git groups to your API, and put it in front: |
| 162 | |
| 163 | ```typescript |
| 164 | class Authentication extends HttpApiMiddleware.Service< |
| 165 | Authentication, |
| 166 | { provides: CurrentUser } |
| 167 | >()("app/Authentication", { error: Unauthorized }) {} |
| 168 | |
| 169 | class AppApi extends HttpApi.make("app") |
| 170 | .addHttpApi(Git.Api) // repos, refs, objects, pulls, protocol, github |
| 171 | .add(AppRoutes) // your own routes beside them |
| 172 | .middleware(Authentication) {} |
| 173 | ``` |
| 174 | |
| 175 | Its implementation is an ordinary Layer — resolve a session cookie, |
| 176 | an API key, or the password `git` sends over HTTP Basic, and provide |
| 177 | `CurrentUser` to the endpoint. `examples/cloudflare-git-service` does |
| 178 | this with Better Auth. |
| 179 | |
| 180 | Because the handlers are yours to call, a route can be overridden |
| 181 | with a few lines of application code. Here `receivePack` reuses the |
| 182 | defaults for the read routes and wraps the push with a branch rule |
| 183 | that reads the `CurrentUser` the middleware provided: |
| 184 | |
| 185 | ```typescript |
| 186 | const ProtocolLive = HttpApiBuilder.group(AppApi, "protocol", (h) => |
| 187 | Effect.gen(function* () { |
| 188 | const git = yield* Git.Engine; |
| 189 | const defaults = yield* Git.Handlers; |
| 190 | return h |
| 191 | .handleRaw("infoRefs", defaults.protocol.infoRefs) |
| 192 | .handleRaw("uploadPack", defaults.protocol.uploadPack) |
| 193 | .handleRaw("receivePack", ({ params, request }) => |
| 194 | Effect.gen(function* () { |
| 195 | const repo = yield* git.repositories.get(params); |
| 196 | const push = yield* GitHttp.ReceivePack.decode(request); |
| 197 | const user = yield* CurrentUser; // provided by Authentication |
| 198 | |
| 199 | if (push.updates.some((u) => u.ref === "refs/heads/main" && user.id !== repo.owner)) { |
| 200 | return GitHttp.ReceivePack.reject(push, "only the owner moves main"); |
| 201 | } |
| 202 | |
| 203 | const prepared = yield* git.preparePush(repo, push.input); |
| 204 | return GitHttp.ReceivePack.response(push, yield* git.commitPush(prepared)); |
| 205 | }), |
| 206 | ); |
| 207 | }), |
| 208 | ); |
| 209 | ``` |
| 210 | |
| 211 | `preparePush` verifies the pack without moving refs; `commitPush` |
| 212 | moves them atomically. The same host runs behind a shared secret, |
| 213 | Better Auth, or anything else — the tutorial walks from one to the |
| 214 | other, ending with the full version of this handler in |
| 215 | [Protect a branch](/git/tutorial/part-6). |
| 216 | |
| 217 | ### Performance |
| 218 | |
| 219 | We benchmarked it against GitHub with real `git` from the same |
| 220 | client and edge: a 36 MiB, 15.6k-object repository, a fresh |
| 221 | repository on each host. |
| 222 | |
| 223 | | Operation | `alchemy/Git` | GitHub | |
| 224 | | --- | --- | --- | |
| 225 | | Full `git clone` | 0.7 s best, 1.3 s typical | 1.3 s best, 2.1 s typical | |
| 226 | | Clone throughput | 80 MiB/s best, 33 MiB/s typical | 28 MiB/s best, 17 MiB/s typical | |
| 227 | | Incremental fetch | 0.4–0.6 s | 0.3–0.5 s | |
| 228 | | Incremental push | 0.2–0.4 s | 1.2 s | |
| 229 | | 44 MiB push, whole blobs | 4.8–6.4 s | 4.6 s | |
| 230 | | 40 MiB push, delta-heavy | 5.9–6.5 s | 3.3 s | |
| 231 | |
| 232 | GitHub's throughput is the pack size divided by its clone time; the |
| 233 | incremental fetch is a `git fetch` with one new commit to pick up. |
| 234 | Clones are fast because the work is done ahead of time. After every |
| 235 | push the repository writes a ready-to-serve bundle to R2, and a |
| 236 | `git clone` is the Worker streaming that file to the client. The |
| 237 | Durable Object is not involved, so the number of concurrent clones a |
| 238 | repository can serve is limited by R2 and Workers, not by one object: |
| 239 | four clones at once reached 93 MiB/s aggregate. Everyday pushes, |
| 240 | which move a few commits, are faster than GitHub; incremental fetches |
| 241 | are about even. Large pushes are the one place GitHub is ahead. The |
| 242 | largest real push so far is the alchemy monorepo, 44k objects in a |
| 243 | 67 MiB pack, hosted on [git.alchemy.run](https://git.alchemy.run) |
| 244 | and cloned back byte-identical under `git fsck --strict`. |
| 245 | |
| 246 | ### Where the hashing runs |
| 247 | |
| 248 | Before a ref moves, every object in a push is inflated, SHA-1 hashed, |
| 249 | and delta-resolved. For a kilobyte push that is nothing; for a 40 MiB |
| 250 | push it is a lot of CPU, and Workers are a poor place for it. A |
| 251 | Worker's CPU is slow for exactly this kind of work — tight loops over |
| 252 | bytes, inflate, and hashing run several times slower than on a |
| 253 | laptop — and there is one thread to run it on: a service binding call |
| 254 | executes on the caller's thread, so fanning out to another Worker |
| 255 | buys nothing. The first version hashed on the request thread and took |
| 256 | 10–13 seconds for the 40 MiB push: |
| 257 | |
| 258 | ```typescript |
| 259 | Layer.provide(Git.HasherInline) |
| 260 | ``` |
| 261 | |
| 262 | `Git.Hasher` is the Layer where a chunk gets hashed, so the fix was a |
| 263 | second implementation, not a change to the engine. |
| 264 | `HasherWorkerLoader` bundles the hasher as a module inside your |
| 265 | Worker and loads it into four fresh isolates per push, each with its |
| 266 | own 128 MB and its own thread. Nothing new is deployed, nothing |
| 267 | leaves Cloudflare, and the same push drops to 5.9–6.5 seconds: |
| 268 | |
| 269 | ```diff lang="typescript" |
| 270 | +import * as GitHasher from "alchemy/Git/Hasher"; |
| 271 | |
| 272 | const GitLive = Git.ApiLive.pipe( |
| 273 | Layer.provide(Git.ApiHandlersLive), |
| 274 | Layer.provide(Authentication.layer), |
| 275 | Layer.provide(Git.ReposDurableObject), |
| 276 | Layer.provide(Git.RegistryDurableObject), |
| 277 | - Layer.provide(Git.HasherInline), |
| 278 | + Layer.provide(GitHasher.HasherWorkerLoader()), |
| 279 | Layer.provide(Git.BlobStoreR2(GitObjects)), |
| 280 | ); |
| 281 | ``` |
| 282 | |
| 283 | `HasherLambda` is the third implementation — one Lambda invocation |
| 284 | per chunk, all at once, on a 3 GB function — for pushes wider than |
| 285 | four chunks when an AWS account is already in the stack. If a hasher |
| 286 | fails, the Worker hashes the chunk inline; a push never fails because |
| 287 | its hasher did. Incremental pushes take 0.2–0.4 s on every one of |
| 288 | them, so start with `HasherInline` and swap when large pushes start |
| 289 | to matter. |
| 290 | |
| 291 | [Scaling](/git/recipes/scaling) and [Hasher](/git/blocks/hasher) |
| 292 | have the full numbers; the design notes in |
| 293 | [`DESIGN.md`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Git/DESIGN.md) |
| 294 | record every step of the sweep. |
| 295 | |
| 296 | It is a git remote you own, with an API, that you can build on — not |
| 297 | a GitHub replacement. No issues, reviews, or Actions. |
| 298 | |
| 299 | Docs: [Git](/git) · [Getting Started](/git/getting-started) · |
| 300 | [Tutorial](/git/tutorial/part-1) · [Building blocks](/git/blocks) · |
| 301 | [Scaling](/git/recipes/scaling). |
| 302 | |
| 303 | ## Stripe |
| 304 | |
| 305 | A new **Stripe provider** for managing Stripe with alchemy |
| 306 | ([#1298](https://github.com/alchemy-run/alchemy/pull/1298)) — thanks |
| 307 | [Michael K](https://github.com/Mkassabov)! It comes with resources |
| 308 | (`Product`, `Price`, `Coupon`, `PromotionCode`, `PaymentLink`, |
| 309 | `BillingPortalConfiguration`, `WebhookEndpoint`, and Connect's |
| 310 | `Account`), bindings for calling Stripe from a Worker |
| 311 | (`CreateCustomer`, `CreateCheckoutSession`, `Retrieve*`, …) that |
| 312 | wire the API key into the Worker for you — each one registers the |
| 313 | permission it needs, so there is no key to copy into `env` — and |
| 314 | `consumeEvents`, an event source that provisions the webhook endpoint |
| 315 | from the Worker's URL and hands your handler a typed event: |
| 316 | |
| 317 | ```typescript |
| 318 | export default class Api extends Cloudflare.Worker<Api>()( |
| 319 | "Api", |
| 320 | { main: import.meta.url }, |
| 321 | Effect.gen(function* () { |
| 322 | const product = yield* Stripe.Product("Pro", { name: "Pro" }); |
| 323 | const price = yield* Stripe.Price("ProMonthly", { |
| 324 | product, |
| 325 | currency: "usd", |
| 326 | unitAmount: 2000, |
| 327 | recurring: { interval: "month" }, |
| 328 | }); |
| 329 | const priceId = yield* price.id; |
| 330 | |
| 331 | const createCustomer = yield* Stripe.CreateCustomer(); |
| 332 | const createCheckout = yield* Stripe.CreateCheckoutSession(); |
| 333 | |
| 334 | yield* Stripe.consumeEvents( |
| 335 | "Events", |
| 336 | { events: [Stripe.CheckoutSessionCompleted, Stripe.InvoicePaymentFailed] }, |
| 337 | Effect.fn(function* (event) { |
| 338 | yield* Effect.log(event.type); |
| 339 | }), |
| 340 | ); |
| 341 | |
| 342 | return { |
| 343 | fetch: Effect.gen(function* () { |
| 344 | const customer = yield* createCustomer({ email: "a@example.com" }); |
| 345 | const session = yield* createCheckout({ |
| 346 | mode: "subscription", |
| 347 | customer: customer.id, |
| 348 | line_items: [{ price: yield* priceId, quantity: 1 }], |
| 349 | success_url: "https://example.com/welcome", |
| 350 | cancel_url: "https://example.com/pricing", |
| 351 | }); |
| 352 | return HttpServerResponse.redirect(session.url!); |
| 353 | }).pipe(Effect.orDie), |
| 354 | }; |
| 355 | }).pipe( |
| 356 | Effect.provide([ |
| 357 | Stripe.CreateCustomerHttp, |
| 358 | Stripe.CreateCheckoutSessionHttp, |
| 359 | Stripe.ConsumeEventsLive, |
| 360 | ]), |
| 361 | ), |
| 362 | ) {} |
| 363 | ``` |
| 364 | |
| 365 | `consumeEvents` verifies signatures (a bad one is a 401) and creates |
| 366 | the `WebhookEndpoint` for you; declare one directly only when you own |
| 367 | the delivery URL. |
| 368 | |
| 369 | Two runnable examples: `examples/stripe-billing` (Checkout, Billing |
| 370 | Portal, and an entitlement record in KV kept current by webhooks) and |
| 371 | `examples/stripe-connect` (Express accounts, hosted onboarding, and |
| 372 | `account.updated` driving a D1 merchant table). |
| 373 | |
| 374 | Docs: [Stripe](/stripe) · [Sell a subscription](/stripe/guides/subscriptions) |
| 375 | · [Onboard merchants with Connect](/stripe/guides/connect) · |
| 376 | [React to Stripe events](/stripe/guides/webhooks). |
| 377 | |
| 378 | ## Worker Previews |
| 379 | |
| 380 | `preview` is the sibling of `version` |
| 381 | ([#1563](https://github.com/alchemy-run/alchemy/pull/1563)): a named |
| 382 | copy of another Worker with its own URL, its own bindings, and |
| 383 | isolated same-Worker Durable Objects. Production traffic is untouched. |
| 384 | |
| 385 | ```typescript |
| 386 | const parent = yield* Cloudflare.Worker.ref("Api", { stage: "prod" }); |
| 387 | |
| 388 | const api = yield* Cloudflare.Worker("Api", { |
| 389 | main: "./src/api.ts", |
| 390 | preview: { of: parent }, |
| 391 | }); |
| 392 | // api.url → https://<stage>-<worker>.<account>.workers.dev |
| 393 | ``` |
| 394 | |
| 395 | The Preview name defaults to the stack stage (`pr-123`). Destroying |
| 396 | the Preview Worker deletes the Preview; the parent stays up. Enable |
| 397 | `domain: { name: "app.example.com", previews: true }` on the parent |
| 398 | and Previews serve at `https://<preview-name>.app.example.com`. |
| 399 | |
| 400 | ```typescript |
| 401 | preview.of // branch / PR — own URL, isolated DOs |
| 402 | version.parent // canary of another stage |
| 403 | version.traffic // gradual rollout of this Worker |
| 404 | version.traffic: 0 // upload without routing |
| 405 | ``` |
| 406 | |
| 407 | Docs: [Worker Previews](/cloudflare/compute/previews). |
| 408 | |
| 409 | ## GitHub: rulesets, protection, releases, and more |
| 410 | |
| 411 | Ten new resources on the GitHub provider. |
| 412 | [`BranchProtection`](/providers/github/branchprotection) |
| 413 | ([#1514](https://github.com/alchemy-run/alchemy/pull/1514)) covers the |
| 414 | classic protection API — thanks |
| 415 | [99andytang](https://github.com/99andytang)! — and |
| 416 | [Agusti F.](https://github.com/agustif) contributed the other nine: |
| 417 | [`Ruleset`](/providers/github/ruleset), |
| 418 | [`Label`](/providers/github/label), |
| 419 | [`Milestone`](/providers/github/milestone), |
| 420 | [`Issue`](/providers/github/issue), |
| 421 | [`PullRequest`](/providers/github/pullrequest), |
| 422 | [`Release`](/providers/github/release), |
| 423 | [`Collaborator`](/providers/github/collaborator), |
| 424 | [`TeamAccess`](/providers/github/teamaccess), and |
| 425 | [`WikiPage`](/providers/github/wikipage) |
| 426 | ([#1565](https://github.com/alchemy-run/alchemy/pull/1565)–[#1578](https://github.com/alchemy-run/alchemy/pull/1578)). |
| 427 | Thanks both! |
| 428 | |
| 429 | ```typescript |
| 430 | yield* GitHub.Ruleset("main-protection", { |
| 431 | owner: "my-org", |
| 432 | repository: "my-repo", |
| 433 | name: "main protection", |
| 434 | target: "branch", |
| 435 | conditions: { include: ["refs/heads/main", "refs/heads/release/*"] }, |
| 436 | rules: { |
| 437 | nonFastForward: true, |
| 438 | deletion: true, |
| 439 | requiredLinearHistory: true, |
| 440 | pullRequest: { |
| 441 | requiredApprovingReviewCount: 2, |
| 442 | requireCodeOwnerReview: true, |
| 443 | dismissStaleReviewsOnPush: true, |
| 444 | }, |
| 445 | }, |
| 446 | }); |
| 447 | |
| 448 | yield* GitHub.TeamAccess("platform", { |
| 449 | owner: "my-org", |
| 450 | repository: "my-repo", |
| 451 | teamSlug: "platform", |
| 452 | permission: "push", |
| 453 | }); |
| 454 | ``` |
| 455 | |
| 456 | Docs: [GitHub](/github). |
| 457 | |
| 458 | ## `alchemy dev` runs lighter |
| 459 | |
| 460 | The `alchemy dev` process tree drops from **3.6 GB to 1.9 GB** on the |
| 461 | Vite example under Node, and a cold import of `alchemy/Cloudflare` |
| 462 | from 523 MB to 214 MB. |
| 463 | |
| 464 | - **One sidecar, not one per provider group** |
| 465 | ([#1613](https://github.com/alchemy-run/alchemy/pull/1613)). A |
| 466 | group is imported the first time a stack uses one of its types, so |
| 467 | a Cloudflare-only stack never loads the AWS emulator. |
| 468 | - **Less engine in every process** |
| 469 | ([#1614](https://github.com/alchemy-run/alchemy/pull/1614)). |
| 470 | `ProviderLayer.dual` stops building its default variant at |
| 471 | registration, the CLI bins skip the `alchemy/Cli` barrel, and the |
| 472 | Cloudflare provider no longer imports all 122 distilled services |
| 473 | for one helper. Sidecar heap: 193 MB → 98 MB. |
| 474 | - **Transforms are cached on disk** |
| 475 | ([#1615](https://github.com/alchemy-run/alchemy/pull/1615)). The |
| 476 | Oxc loader writes output to a per-user cache keyed by path, size, |
| 477 | mtime, and tsconfig instead of re-transpiling the source graph |
| 478 | (with inline source maps) in every Node process. |
| 479 | `ALCHEMY_TRANSFORM_CACHE=0` disables it. |
| 480 | - **The workerd proxy is a `node:net` byte pipe** |
| 481 | ([#1612](https://github.com/alchemy-run/alchemy/pull/1612)) instead |
| 482 | of a second workerd per Worker. HTTP/1.1, streaming, and WebSockets |
| 483 | pass through untouched; the Worker sees the real `Host`, so |
| 484 | `request.url` is the public URL |
| 485 | ([#1573](https://github.com/alchemy-run/alchemy/pull/1573)); a |
| 486 | request during a broken build gets a 502 with the build error. |
| 487 | |
| 488 | Two beta.77 crashes are fixed too: `alchemy dev` under Bun could not |
| 489 | import any stack because the dev probe intercepted the root's own |
| 490 | `node_modules` |
| 491 | ([#1617](https://github.com/alchemy-run/alchemy/pull/1617), thanks |
| 492 | [Austin](https://github.com/austinm911)!), and the interactive CLI died |
| 493 | with `ReactSharedInternals.H.useMemo` when a consumer's install hoisted |
| 494 | a different React — Sigil now bundles its own |
| 495 | ([#1636](https://github.com/alchemy-run/alchemy/pull/1636)). |
| 496 | |
| 497 | ## RDS, S3, and security group fixes |
| 498 | |
| 499 | A run of provider fixes from |
| 500 | [Bjorn Pagen](https://github.com/bjornpagen) and |
| 501 | [Henning Pokriefke](https://github.com/henningpokriefke) — thanks |
| 502 | both! — closes gaps where RDS, S3, and EC2 security groups trusted |
| 503 | state instead of the cloud: drift went unrepaired on unchanged |
| 504 | deploys, and removing a prop left the old value in place instead of |
| 505 | restoring the documented default. |
| 506 | |
| 507 | **Security groups** apply rule deltas instead of revoke-and-recreate |
| 508 | ([#1592](https://github.com/alchemy-run/alchemy/pull/1592), |
| 509 | [#1664](https://github.com/alchemy-run/alchemy/pull/1664)), and an |
| 510 | inline-rule update no longer deletes `SecurityGroupRule` resources |
| 511 | attached to the same group |
| 512 | ([#1624](https://github.com/alchemy-run/alchemy/pull/1624)). |
| 513 | `egress` has three distinct states |
| 514 | ([#1623](https://github.com/alchemy-run/alchemy/pull/1623)): |
| 515 | |
| 516 | ```typescript |
| 517 | // Omitted: AWS's default — allow all outbound IPv4. |
| 518 | AWS.EC2.SecurityGroup("DefaultEgress", { vpcId }); |
| 519 | |
| 520 | // Explicitly empty: no outbound traffic. |
| 521 | AWS.EC2.SecurityGroup("NoEgress", { vpcId, egress: [] }); |
| 522 | |
| 523 | // Non-empty: only what is listed. |
| 524 | AWS.EC2.SecurityGroup("HttpsEgress", { |
| 525 | vpcId, |
| 526 | egress: [{ ipProtocol: "tcp", fromPort: 443, toPort: 443, cidrIpv4: "0.0.0.0/0" }], |
| 527 | }); |
| 528 | ``` |
| 529 | |
| 530 | **RDS** `DBInstance` reconciles storage as one configuration — |
| 531 | allocation, type, IOPS, throughput, and the autoscaling limit sent |
| 532 | together when AWS requires it |
| 533 | ([#1593](https://github.com/alchemy-run/alchemy/pull/1593), |
| 534 | [#1594](https://github.com/alchemy-run/alchemy/pull/1594)) — compares |
| 535 | the observed endpoint port |
| 536 | ([#1595](https://github.com/alchemy-run/alchemy/pull/1595)), and |
| 537 | restores the engine-default parameter group and the VPC default |
| 538 | security group when their associations are removed |
| 539 | ([#1596](https://github.com/alchemy-run/alchemy/pull/1596)). |
| 540 | `DBParameterGroup` converges to declared overrides and reads back |
| 541 | settled values |
| 542 | ([#1589](https://github.com/alchemy-run/alchemy/pull/1589), |
| 543 | [#1590](https://github.com/alchemy-run/alchemy/pull/1590)). |
| 544 | |
| 545 | ```typescript |
| 546 | yield* AWS.RDS.DBInstance("Db", { |
| 547 | engine: "postgres", |
| 548 | dbInstanceClass: "db.t3.micro", |
| 549 | masterUsername: "admin", |
| 550 | manageMasterUserPassword: true, |
| 551 | allocatedStorage: 20, // a minimum — RDS cannot shrink storage |
| 552 | maxAllocatedStorage: 100, // remove it to disable autoscaling |
| 553 | }); |
| 554 | ``` |
| 555 | |
| 556 | **S3** buckets and `AWS.state()` gain |
| 557 | `encryption.blockedEncryptionTypes: ["SSE-C"]` |
| 558 | ([#1588](https://github.com/alchemy-run/alchemy/pull/1588)), compare |
| 559 | decoded KMS key values so matching keys skip `PutBucketEncryption` |
| 560 | ([#1587](https://github.com/alchemy-run/alchemy/pull/1587)), propagate |
| 561 | configuration read failures instead of reconciling over them |
| 562 | ([#1586](https://github.com/alchemy-run/alchemy/pull/1586)), and check |
| 563 | ownership with `GetBucketLocation` so configuration-only operators no |
| 564 | longer need `s3:ListBucket` |
| 565 | ([#1585](https://github.com/alchemy-run/alchemy/pull/1585)). |
| 566 | |
| 567 | Docs: [RDS](/aws/data/rds) · [S3](/aws/data/s3). |
| 568 | |
| 569 | ## Also in this release |
| 570 | |
| 571 | - **Preview packages at `pkg.alchemy.run`** |
| 572 | ([#1516](https://github.com/alchemy-run/alchemy/pull/1516)) — every |
| 573 | PR, branch, and commit publishes installable packages from a |
| 574 | Cloudflare Worker registry that verifies each publication against |
| 575 | the GitHub Actions run that built it, so fork PRs publish with no |
| 576 | secrets. Dependencies are content-addressed, so shared packages |
| 577 | deduplicate across PRs. |
| 578 | |
| 579 | ```sh |
| 580 | pnpm install https://pkg.alchemy.run/alchemy/pr:1516 |
| 581 | pnpm install https://pkg.alchemy.run/alchemy/branch:main |
| 582 | ``` |
| 583 | |
| 584 | - **Typed named entrypoint bindings** |
| 585 | ([#1415](https://github.com/alchemy-run/alchemy/pull/1415)) — |
| 586 | `Cloudflare.WorkerEntrypoint<Api>(target, "Api")` types `env.API` |
| 587 | as `Service<Api>`, so `env.API.greet("alice")` typechecks. Thanks |
| 588 | [Michael K](https://github.com/Mkassabov)! |
| 589 | - **Railway sandboxes: domains, sizing, forks, checkpoints** |
| 590 | ([#1661](https://github.com/alchemy-run/alchemy/pull/1661)) — |
| 591 | `publicDomains`, `resources: { cpu, memoryGB }`, and |
| 592 | `Railway.SandboxCheckpoint` to snapshot a prepared box and restore |
| 593 | it as a `template`. Railway reconcilers now select only the GraphQL |
| 594 | fields they need ([#1604](https://github.com/alchemy-run/alchemy/pull/1604)), |
| 595 | and `MountVolume` actually attaches the disk |
| 596 | ([#1584](https://github.com/alchemy-run/alchemy/pull/1584)). |
| 597 | - **Prisma Compute serves static apps** |
| 598 | ([#1208](https://github.com/alchemy-run/alchemy/pull/1208)) — |
| 599 | `build: "auto"` recognizes Vite SPA output and adds the Bun |
| 600 | entrypoint Prisma requires; `build: { type: "static", spa: true }` |
| 601 | serves a prebuilt directory. Thanks |
| 602 | [Aman Varshney](https://github.com/AmanVarshney01)! The Prisma |
| 603 | provider also moves onto the distilled SDK |
| 604 | ([#1290](https://github.com/alchemy-run/alchemy/pull/1290), |
| 605 | [#1669](https://github.com/alchemy-run/alchemy/pull/1669)) — thanks |
| 606 | [Will Madden](https://github.com/wmadden)! |
| 607 | - **Cloudflare Containers** — a batch from |
| 608 | [Dan van der Merwe](https://github.com/danieljvdm), thanks! |
| 609 | `memoryMib` is forwarded |
| 610 | ([#1521](https://github.com/alchemy-run/alchemy/pull/1521)), |
| 611 | Dockerfile builds export straight to the registry with |
| 612 | `buildx build --push` |
| 613 | ([#1524](https://github.com/alchemy-run/alchemy/pull/1524)), |
| 614 | identical images publish once per deploy |
| 615 | ([#1525](https://github.com/alchemy-run/alchemy/pull/1525)), a |
| 616 | cached identity that no longer matches Cloudflare plans a visible |
| 617 | replacement instead of erroring |
| 618 | ([#1523](https://github.com/alchemy-run/alchemy/pull/1523)), |
| 619 | interrupted creates recover by generated name |
| 620 | ([#1526](https://github.com/alchemy-run/alchemy/pull/1526)), and an |
| 621 | unresolved Worker namespace no longer detaches the container's DO |
| 622 | ([#1150](https://github.com/alchemy-run/alchemy/pull/1150)). The |
| 623 | dev egress interceptor pulls host-native, fixing containers on |
| 624 | Apple Silicon |
| 625 | ([#1459](https://github.com/alchemy-run/alchemy/pull/1459)) — thanks |
| 626 | [Erik Müller](https://github.com/asterikx)! |
| 627 | - **R2 `put` forwards options on the stream path** |
| 628 | ([#1602](https://github.com/alchemy-run/alchemy/pull/1602)) — a |
| 629 | declared `sha256`/`md5` on an Effect `Stream` body is now verified |
| 630 | by R2 instead of silently dropped. |
| 631 | - **Crons fire in `Website.Vite` dev workers** |
| 632 | ([#1641](https://github.com/alchemy-run/alchemy/pull/1641)). Thanks |
| 633 | [zawaki](https://github.com/zawaki)! |
| 634 | - **`alchemy/Cloudflare/Bridge`** |
| 635 | ([#1643](https://github.com/alchemy-run/alchemy/pull/1643)) — the |
| 636 | Worker bridge generated wrappers import is a runtime-only entry, so |
| 637 | deploy tooling stays out of the bundle. Thanks |
| 638 | [Alex](https://github.com/agcty)! |
| 639 | - **EC2 instances with a fixed private IP replace delete-first** |
| 640 | ([#1667](https://github.com/alchemy-run/alchemy/pull/1667)) — the |
| 641 | successor can't take the address while the predecessor holds it. |
| 642 | Thanks [dawson](https://github.com/Dawsson)! |
| 643 | - **AWS Organizations rewrite missing ownership tags** |
| 644 | ([#1637](https://github.com/alchemy-run/alchemy/pull/1637)) instead |
| 645 | of reporting them restored. Thanks |
| 646 | [Henning Pokriefke](https://github.com/henningpokriefke)! |
| 647 | - **Log group ARNs from state are normalized before tagging** |
| 648 | ([#1575](https://github.com/alchemy-run/alchemy/pull/1575)) — a |
| 649 | `:*` suffix persisted by an old adoption no longer fails every |
| 650 | converge with `Invalid resourceArn`. Thanks |
| 651 | [Evan Spaeder](https://github.com/evanspaeder)! |
| 652 | - **ECR images skip updates when only the context directory moves** |
| 653 | ([#1591](https://github.com/alchemy-run/alchemy/pull/1591)). |
| 654 | - **Destroy converges after drifted state** — `SNS.Subscription` |
| 655 | and `DynamoDB.Table` tolerate an already-deleted parent |
| 656 | ([#1576](https://github.com/alchemy-run/alchemy/pull/1576)), and |
| 657 | `ECS.Service` deletes past a draining service |
| 658 | ([#1580](https://github.com/alchemy-run/alchemy/pull/1580)). |
| 659 | Hetzner servers and their deploy keys recover after an interrupted |
| 660 | create ([#1662](https://github.com/alchemy-run/alchemy/pull/1662)). |
| 661 | - **HTTP state-store errors carry no payloads** |
| 662 | ([#1522](https://github.com/alchemy-run/alchemy/pull/1522)) — a |
| 663 | failed write reports a failure kind and status, never the request |
| 664 | or response body. Thanks |
| 665 | [Dan van der Merwe](https://github.com/danieljvdm)! |
| 666 | - **CLI fixes** — the published `alchemy dev` resolves its sidecar |
| 667 | through package exports |
| 668 | ([#1678](https://github.com/alchemy-run/alchemy/pull/1678)), colors |
| 669 | follow the terminal's detected profile instead of forcing truecolor |
| 670 | ([#1679](https://github.com/alchemy-run/alchemy/pull/1679)), and |
| 671 | the exec bundle keeps its `.js` extension |
| 672 | ([#1625](https://github.com/alchemy-run/alchemy/pull/1625)). |
| 673 | - **Smaller dependency closure** — `jszip` → `fflate` |
| 674 | ([#1608](https://github.com/alchemy-run/alchemy/pull/1608)), |
| 675 | `fast-glob` → `tinyglobby` |
| 676 | ([#1609](https://github.com/alchemy-run/alchemy/pull/1609)), and |
| 677 | Foldkit `^0.160.0` for effect rc.115 |
| 678 | ([#1618](https://github.com/alchemy-run/alchemy/pull/1618)). |
| 679 | - **Core docs read as one narrative** |
| 680 | ([#1652](https://github.com/alchemy-run/alchemy/pull/1652)) — each |
| 681 | concept has one owning page; `/infrastructure-as-effects` redirects |
| 682 | to [What is Alchemy](/what-is-alchemy). |
| 683 | |
| 684 | ## Where to go next |
| 685 | |
| 686 | - [Git](/git) · [Getting Started](/git/getting-started) · [Tutorial](/git/tutorial/part-1) |
| 687 | - [Stripe](/stripe) · [Sell a subscription](/stripe/guides/subscriptions) · [Connect](/stripe/guides/connect) |
| 688 | - [Worker Previews](/cloudflare/compute/previews) |
| 689 | - [GitHub](/github) |
| 690 | - [Local development](/environments/local-development) |
| 691 | - [RDS](/aws/data/rds) · [S3](/aws/data/s3) |
| 692 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta78) |
| 693 | - [Compare v2.0.0-beta.77 → v2.0.0-beta.78](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.77...v2.0.0-beta.78) |