2.0.0-beta.78 - Git Hosting, Stripe & Worker Previews
beta.78 ships alchemy/Git, an embeddable, pluggable git server
built from Effect Layers. Mount it inside any Effect HTTP API and
pick where refs and bytes live — Durable Objects and R2 on
Cloudflare, S3 and Lambda on AWS, or a store of your own. A new
Stripe provider provisions Stripe resources and binds them to
your Workers and Functions. Worker Previews give every branch its
own URL and isolated Durable Objects. The GitHub provider gains ten
resources, alchemy dev runs on half the memory, and a round of
fixes to RDS, S3, and security groups repairs drift and restores
defaults when props are removed.
effect rc.115
Section titled “effect rc.115”alchemy now requires effect >=4.0.0-rc.115
(#1562,
#1601):
bun add alchemy@latest effect@rc @effect/platform-bun@rc @effect/platform-node@rcrc.113 renamed the Config and Flag constructors to PascalCase:
Config.string("HOST")Config.String("HOST")Config.mapOrFail(...)Config.mapEffect(...)Flag.boolean("yes")Flag.Boolean("yes")Flag.choice("backend", …)Flag.Literals("backend", …)rc.113 also replaced @effect/sql-pg’s pg dependency with a native
wire client, which sent no SNI and rejected sslmode=prefer. Both
broke Aurora DSQL, Neon, and local Hyperdrive; alchemy’s
SQL.Postgres, Drizzle.Postgres, and postgresState now derive the
TLS config from the URL
(#1580,
#1582), and rc.115
fixes the SNI half upstream
(Effect-TS/effect#8174).
An embeddable, pluggable Git server
Section titled “An embeddable, pluggable Git server”alchemy/Git is a pluggable, embeddable git server
(#1187,
#1621). It speaks
smart HTTP to any git client, serves a typed REST plane with pull
requests, and answers the GitHub REST v3 API so gh api and Octokit
work against it unchanged. Nothing ships a Worker — every part is an
Effect service with swappable Layers, assembled into a Worker you own:
import * as Git from "alchemy/Git";import * as Http from "alchemy/Http";
export const GitObjects = Cloudflare.R2.Bucket("GitObjects");
const GitLive = Git.ApiLive.pipe( Layer.provide(Git.ApiHandlersLive), // the engine's route handlers Layer.provide(Authentication.layer), // yours: who may call what Layer.provide(Git.ReposDurableObject), // refs, objects, pull requests Layer.provide(Git.RegistryDurableObject), // owner/name → repo Layer.provide(Git.HasherInline), // push verification Layer.provide(Git.BlobStoreR2(GitObjects)), // packs, bundles, large pushes);
export default Cloudflare.Worker( "Git", { main: import.meta.url, ...Git.GIT_WORKER_OPTIONS }, Effect.gen(function* () { const fetch = yield* HttpRouter.toHttpEffect( GitLive.pipe(Layer.provide(Http.Platform)), ); return { fetch }; }),);Each line is one decision with its own implementations. Change a line and only that decision changes — here the bytes move to S3:
const GitObjects = Cloudflare.R2.Bucket("GitObjects");const GitObjects = AWS.S3.Bucket("GitObjects");
const GitLive = Git.ApiLive.pipe( Layer.provide(Git.ApiHandlersLive), Layer.provide(Authentication.layer), Layer.provide(Git.ReposDurableObject), Layer.provide(Git.RegistryDurableObject), Layer.provide(Git.HasherInline), Layer.provide(Git.BlobStoreR2(GitObjects)), Layer.provide(Git.BlobStoreS3(GitObjects)),);Embedded by design
Section titled “Embedded by design”You build the HTTP server and mount alchemy/Git’s routes into it.
Git.Api is an Effect HttpApi group and Git.Handlers supplies
the default implementation of every route as a plain function. Add
the group to your own API and put your own HttpApiMiddleware in
front of it, and every request — git clone and git push, the REST
API, and the GitHub-compatible API gh uses — goes through your
authentication and your user management. The engine itself holds no
users, no credentials, and no policy.
The middleware is native HttpApiMiddleware. Declare what it
provides, add the Git groups to your API, and put it in front:
class Authentication extends HttpApiMiddleware.Service< Authentication, { provides: CurrentUser }>()("app/Authentication", { error: Unauthorized }) {}
class AppApi extends HttpApi.make("app") .addHttpApi(Git.Api) // repos, refs, objects, pulls, protocol, github .add(AppRoutes) // your own routes beside them .middleware(Authentication) {}Its implementation is an ordinary Layer — resolve a session cookie,
an API key, or the password git sends over HTTP Basic, and provide
CurrentUser to the endpoint. examples/cloudflare-git-service does
this with Better Auth.
Because the handlers are yours to call, a route can be overridden
with a few lines of application code. Here receivePack reuses the
defaults for the read routes and wraps the push with a branch rule
that reads the CurrentUser the middleware provided:
const ProtocolLive = HttpApiBuilder.group(AppApi, "protocol", (h) => Effect.gen(function* () { const git = yield* Git.Engine; const defaults = yield* Git.Handlers; return h .handleRaw("infoRefs", defaults.protocol.infoRefs) .handleRaw("uploadPack", defaults.protocol.uploadPack) .handleRaw("receivePack", ({ params, request }) => Effect.gen(function* () { const repo = yield* git.repositories.get(params); const push = yield* GitHttp.ReceivePack.decode(request); const user = yield* CurrentUser; // provided by Authentication
if (push.updates.some((u) => u.ref === "refs/heads/main" && user.id !== repo.owner)) { return GitHttp.ReceivePack.reject(push, "only the owner moves main"); }
const prepared = yield* git.preparePush(repo, push.input); return GitHttp.ReceivePack.response(push, yield* git.commitPush(prepared)); }), ); }),);preparePush verifies the pack without moving refs; commitPush
moves them atomically. The same host runs behind a shared secret,
Better Auth, or anything else — the tutorial walks from one to the
other, ending with the full version of this handler in
Protect a branch.
Performance
Section titled “Performance”We benchmarked it against GitHub with real git from the same
client and edge: a 36 MiB, 15.6k-object repository, a fresh
repository on each host.
alchemy/Git |
GitHub | |
|---|---|---|
Full git clone |
0.7 s best, 1.3 s typical | 1.3 s best, 2.1 s typical |
| Clone throughput | 80 MiB/s best, 33 MiB/s typical | 28 MiB/s best, 17 MiB/s typical |
| Incremental fetch | 0.4–0.6 s | 0.3–0.5 s |
| Incremental push | 0.2–0.4 s | 1.2 s |
| 44 MiB push, whole blobs | 4.8–6.4 s | 4.6 s |
| 40 MiB push, delta-heavy | 5.9–6.5 s | 3.3 s |
GitHub’s throughput is the pack size divided by its clone time; the
incremental fetch is a git fetch with one new commit to pick up.
Clones are fast because the work is done ahead of time. After every
push the repository writes a ready-to-serve bundle to R2, and a
git clone is the Worker streaming that file to the client. The
Durable Object is not involved, so the number of concurrent clones a
repository can serve is limited by R2 and Workers, not by one object:
four clones at once reached 93 MiB/s aggregate. Everyday pushes,
which move a few commits, are faster than GitHub; incremental fetches
are about even. Large pushes are the one place GitHub is ahead. The
largest real push so far is the alchemy monorepo, 44k objects in a
67 MiB pack, hosted on git.alchemy.run
and cloned back byte-identical under git fsck --strict.
Where the hashing runs
Section titled “Where the hashing runs”Before a ref moves, every object in a push is inflated, SHA-1 hashed, and delta-resolved. For a kilobyte push that is nothing; for a 40 MiB push it is a lot of CPU, and Workers are a poor place for it. A Worker’s CPU is slow for exactly this kind of work — tight loops over bytes, inflate, and hashing run several times slower than on a laptop — and there is one thread to run it on: a service binding call executes on the caller’s thread, so fanning out to another Worker buys nothing. The first version hashed on the request thread and took 10–13 seconds for the 40 MiB push:
Layer.provide(Git.HasherInline)Git.Hasher is the Layer where a chunk gets hashed, so the fix was a
second implementation, not a change to the engine.
HasherWorkerLoader bundles the hasher as a module inside your
Worker and loads it into four fresh isolates per push, each with its
own 128 MB and its own thread. Nothing new is deployed, nothing
leaves Cloudflare, and the same push drops to 5.9–6.5 seconds:
import * as GitHasher from "alchemy/Git/Hasher";
const GitLive = Git.ApiLive.pipe( Layer.provide(Git.ApiHandlersLive), Layer.provide(Authentication.layer), Layer.provide(Git.ReposDurableObject), Layer.provide(Git.RegistryDurableObject), Layer.provide(Git.HasherInline), Layer.provide(GitHasher.HasherWorkerLoader()), Layer.provide(Git.BlobStoreR2(GitObjects)),);HasherLambda is the third implementation — one Lambda invocation
per chunk, all at once, on a 3 GB function — for pushes wider than
four chunks when an AWS account is already in the stack. If a hasher
fails, the Worker hashes the chunk inline; a push never fails because
its hasher did. Incremental pushes take 0.2–0.4 s on every one of
them, so start with HasherInline and swap when large pushes start
to matter.
Scaling and Hasher
have the full numbers; the design notes in
DESIGN.md
record every step of the sweep.
It is a git remote you own, with an API, that you can build on — not a GitHub replacement. No issues, reviews, or Actions.
Docs: Git · Getting Started · Tutorial · Building blocks · Scaling.
Stripe
Section titled “Stripe”A new Stripe provider for managing Stripe with alchemy
(#1298) — thanks
Michael K! It comes with resources
(Product, Price, Coupon, PromotionCode, PaymentLink,
BillingPortalConfiguration, WebhookEndpoint, and Connect’s
Account), bindings for calling Stripe from a Worker
(CreateCustomer, CreateCheckoutSession, Retrieve*, …) that
wire the API key into the Worker for you — each one registers the
permission it needs, so there is no key to copy into env — and
consumeEvents, an event source that provisions the webhook endpoint
from the Worker’s URL and hands your handler a typed event:
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const product = yield* Stripe.Product("Pro", { name: "Pro" }); const price = yield* Stripe.Price("ProMonthly", { product, currency: "usd", unitAmount: 2000, recurring: { interval: "month" }, }); const priceId = yield* price.id;
const createCustomer = yield* Stripe.CreateCustomer(); const createCheckout = yield* Stripe.CreateCheckoutSession();
yield* Stripe.consumeEvents( "Events", { events: [Stripe.CheckoutSessionCompleted, Stripe.InvoicePaymentFailed] }, Effect.fn(function* (event) { yield* Effect.log(event.type); }), );
return { fetch: Effect.gen(function* () { const customer = yield* createCustomer({ email: "a@example.com" }); const session = yield* createCheckout({ mode: "subscription", customer: customer.id, line_items: [{ price: yield* priceId, quantity: 1 }], success_url: "https://example.com/welcome", cancel_url: "https://example.com/pricing", }); return HttpServerResponse.redirect(session.url!); }).pipe(Effect.orDie), }; }).pipe( Effect.provide([ Stripe.CreateCustomerHttp, Stripe.CreateCheckoutSessionHttp, Stripe.ConsumeEventsLive, ]), ),) {}consumeEvents verifies signatures (a bad one is a 401) and creates
the WebhookEndpoint for you; declare one directly only when you own
the delivery URL.
Two runnable examples: examples/stripe-billing (Checkout, Billing
Portal, and an entitlement record in KV kept current by webhooks) and
examples/stripe-connect (Express accounts, hosted onboarding, and
account.updated driving a D1 merchant table).
Docs: Stripe · Sell a subscription · Onboard merchants with Connect · React to Stripe events.
Worker Previews
Section titled “Worker Previews”preview is the sibling of version
(#1563): a named
copy of another Worker with its own URL, its own bindings, and
isolated same-Worker Durable Objects. Production traffic is untouched.
const parent = yield* Cloudflare.Worker.ref("Api", { stage: "prod" });
const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", preview: { of: parent },});// api.url → https://<stage>-<worker>.<account>.workers.devThe Preview name defaults to the stack stage (pr-123). Destroying
the Preview Worker deletes the Preview; the parent stays up. Enable
domain: { name: "app.example.com", previews: true } on the parent
and Previews serve at https://<preview-name>.app.example.com.
preview.of // branch / PR — own URL, isolated DOsversion.parent // canary of another stageversion.traffic // gradual rollout of this Workerversion.traffic: 0 // upload without routingDocs: Worker Previews.
GitHub: rulesets, protection, releases, and more
Section titled “GitHub: rulesets, protection, releases, and more”Ten new resources on the GitHub provider.
BranchProtection
(#1514) covers the
classic protection API — thanks
99andytang! — and
Agusti F. contributed the other nine:
Ruleset,
Label,
Milestone,
Issue,
PullRequest,
Release,
Collaborator,
TeamAccess, and
WikiPage
(#1565–#1578).
Thanks both!
yield* GitHub.Ruleset("main-protection", { owner: "my-org", repository: "my-repo", name: "main protection", target: "branch", conditions: { include: ["refs/heads/main", "refs/heads/release/*"] }, rules: { nonFastForward: true, deletion: true, requiredLinearHistory: true, pullRequest: { requiredApprovingReviewCount: 2, requireCodeOwnerReview: true, dismissStaleReviewsOnPush: true, }, },});
yield* GitHub.TeamAccess("platform", { owner: "my-org", repository: "my-repo", teamSlug: "platform", permission: "push",});Docs: GitHub.
alchemy dev runs lighter
Section titled “alchemy dev runs lighter”The alchemy dev process tree drops from 3.6 GB to 1.9 GB on the
Vite example under Node, and a cold import of alchemy/Cloudflare
from 523 MB to 214 MB.
- One sidecar, not one per provider group (#1613). A group is imported the first time a stack uses one of its types, so a Cloudflare-only stack never loads the AWS emulator.
- Less engine in every process
(#1614).
ProviderLayer.dualstops building its default variant at registration, the CLI bins skip thealchemy/Clibarrel, and the Cloudflare provider no longer imports all 122 distilled services for one helper. Sidecar heap: 193 MB → 98 MB. - Transforms are cached on disk
(#1615). The
Oxc loader writes output to a per-user cache keyed by path, size,
mtime, and tsconfig instead of re-transpiling the source graph
(with inline source maps) in every Node process.
ALCHEMY_TRANSFORM_CACHE=0disables it. - The workerd proxy is a
node:netbyte pipe (#1612) instead of a second workerd per Worker. HTTP/1.1, streaming, and WebSockets pass through untouched; the Worker sees the realHost, sorequest.urlis the public URL (#1573); a request during a broken build gets a 502 with the build error.
Two beta.77 crashes are fixed too: alchemy dev under Bun could not
import any stack because the dev probe intercepted the root’s own
node_modules
(#1617, thanks
Austin!), and the interactive CLI died
with ReactSharedInternals.H.useMemo when a consumer’s install hoisted
a different React — Sigil now bundles its own
(#1636).
RDS, S3, and security group fixes
Section titled “RDS, S3, and security group fixes”A run of provider fixes from Bjorn Pagen and Henning Pokriefke — thanks both! — closes gaps where RDS, S3, and EC2 security groups trusted state instead of the cloud: drift went unrepaired on unchanged deploys, and removing a prop left the old value in place instead of restoring the documented default.
Security groups apply rule deltas instead of revoke-and-recreate
(#1592,
#1664), and an
inline-rule update no longer deletes SecurityGroupRule resources
attached to the same group
(#1624).
egress has three distinct states
(#1623):
// Omitted: AWS's default — allow all outbound IPv4.AWS.EC2.SecurityGroup("DefaultEgress", { vpcId });
// Explicitly empty: no outbound traffic.AWS.EC2.SecurityGroup("NoEgress", { vpcId, egress: [] });
// Non-empty: only what is listed.AWS.EC2.SecurityGroup("HttpsEgress", { vpcId, egress: [{ ipProtocol: "tcp", fromPort: 443, toPort: 443, cidrIpv4: "0.0.0.0/0" }],});RDS DBInstance reconciles storage as one configuration —
allocation, type, IOPS, throughput, and the autoscaling limit sent
together when AWS requires it
(#1593,
#1594) — compares
the observed endpoint port
(#1595), and
restores the engine-default parameter group and the VPC default
security group when their associations are removed
(#1596).
DBParameterGroup converges to declared overrides and reads back
settled values
(#1589,
#1590).
yield* AWS.RDS.DBInstance("Db", { engine: "postgres", dbInstanceClass: "db.t3.micro", masterUsername: "admin", manageMasterUserPassword: true, allocatedStorage: 20, // a minimum — RDS cannot shrink storage maxAllocatedStorage: 100, // remove it to disable autoscaling});S3 buckets and AWS.state() gain
encryption.blockedEncryptionTypes: ["SSE-C"]
(#1588), compare
decoded KMS key values so matching keys skip PutBucketEncryption
(#1587), propagate
configuration read failures instead of reconciling over them
(#1586), and check
ownership with GetBucketLocation so configuration-only operators no
longer need s3:ListBucket
(#1585).
Also in this release
Section titled “Also in this release”-
Preview packages at
pkg.alchemy.run(#1516) — every PR, branch, and commit publishes installable packages from a Cloudflare Worker registry that verifies each publication against the GitHub Actions run that built it, so fork PRs publish with no secrets. Dependencies are content-addressed, so shared packages deduplicate across PRs.Terminal window pnpm install https://pkg.alchemy.run/alchemy/pr:1516pnpm install https://pkg.alchemy.run/alchemy/branch:main -
Typed named entrypoint bindings (#1415) —
Cloudflare.WorkerEntrypoint<Api>(target, "Api")typesenv.APIasService<Api>, soenv.API.greet("alice")typechecks. Thanks Michael K! -
Railway sandboxes: domains, sizing, forks, checkpoints (#1661) —
publicDomains,resources: { cpu, memoryGB }, andRailway.SandboxCheckpointto snapshot a prepared box and restore it as atemplate. Railway reconcilers now select only the GraphQL fields they need (#1604), andMountVolumeactually attaches the disk (#1584). -
Prisma Compute serves static apps (#1208) —
build: "auto"recognizes Vite SPA output and adds the Bun entrypoint Prisma requires;build: { type: "static", spa: true }serves a prebuilt directory. Thanks Aman Varshney! The Prisma provider also moves onto the distilled SDK (#1290, #1669) — thanks Will Madden! -
Cloudflare Containers — a batch from Dan van der Merwe, thanks!
memoryMibis forwarded (#1521), Dockerfile builds export straight to the registry withbuildx build --push(#1524), identical images publish once per deploy (#1525), a cached identity that no longer matches Cloudflare plans a visible replacement instead of erroring (#1523), interrupted creates recover by generated name (#1526), and an unresolved Worker namespace no longer detaches the container’s DO (#1150). The dev egress interceptor pulls host-native, fixing containers on Apple Silicon (#1459) — thanks Erik Müller! -
R2
putforwards options on the stream path (#1602) — a declaredsha256/md5on an EffectStreambody is now verified by R2 instead of silently dropped. -
Crons fire in
Website.Vitedev workers (#1641). Thanks zawaki! -
alchemy/Cloudflare/Bridge(#1643) — the Worker bridge generated wrappers import is a runtime-only entry, so deploy tooling stays out of the bundle. Thanks Alex! -
EC2 instances with a fixed private IP replace delete-first (#1667) — the successor can’t take the address while the predecessor holds it. Thanks dawson!
-
AWS Organizations rewrite missing ownership tags (#1637) instead of reporting them restored. Thanks Henning Pokriefke!
-
Log group ARNs from state are normalized before tagging (#1575) — a
:*suffix persisted by an old adoption no longer fails every converge withInvalid resourceArn. Thanks Evan Spaeder! -
ECR images skip updates when only the context directory moves (#1591).
-
Destroy converges after drifted state —
SNS.SubscriptionandDynamoDB.Tabletolerate an already-deleted parent (#1576), andECS.Servicedeletes past a draining service (#1580). Hetzner servers and their deploy keys recover after an interrupted create (#1662). -
HTTP state-store errors carry no payloads (#1522) — a failed write reports a failure kind and status, never the request or response body. Thanks Dan van der Merwe!
-
CLI fixes — the published
alchemy devresolves its sidecar through package exports (#1678), colors follow the terminal’s detected profile instead of forcing truecolor (#1679), and the exec bundle keeps its.jsextension (#1625). -
Smaller dependency closure —
jszip→fflate(#1608),fast-glob→tinyglobby(#1609), and Foldkit^0.160.0for effect rc.115 (#1618). -
Core docs read as one narrative (#1652) — each concept has one owning page;
/infrastructure-as-effectsredirects to What is Alchemy.