Zero → production.
Pure TypeScript Infrastructure as Code. For frontends, backends, and the cloud they run on.
Plan, deploy, destroy.
Declare resources inside an Alchemy.Stack. alchemy plan shows you exactly what will change. deploy applies it. destroy reverses it. Learn more →
// alchemy.run.ts — the entrypoint export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers() }, Effect.gen(function* () { yield* Photos; yield* Sessions; const api = yield* Api; return { url: api.url }; }), );
$
The IAM policy writes itself.
Least-privilege IAM, generated from how you use resources in TypeScript. Connect a Worker to an R2 bucket, a DynamoDB table, an SQS queue — Alchemy emits the exact policy. Subscribe to a stream and you get the EventSourceMapping with the right permissions, automatically. Learn more →
export default class JobApi extends AWS.Lambda.Function<JobApi>()( "JobApi", Effect.gen(function* () { const get = yield* S3.GetObject.bind(Photos); const put = yield* DynamoDB.PutItem.bind(Jobs); yield* DynamoDB.stream(Jobs).process(handler); // handler uses get / put / stream … }), ) {}
Workers run locally. Resources run live.
alchemy dev deploys your R2 buckets, KV namespaces, D1 databases — the actual cloud resources — and runs your Worker code locally in workerd, the same runtime as production. Edit a file, the Worker reloads. Add a resource, alchemy diffs and wires it. Learn more →
workerd as a local process. Set breakpoints, inspect variables, profile.Tests deploy. Tests destroy. One stack per suite.
A Stack is just an Effect, so you can yield it from a test. deploy in beforeAll, destroy in afterAll. Each suite gets its own stage; runs in parallel without collision. Learn more →
import { afterAll, beforeAll, deploy, destroy, expect, test } from "alchemy/Test/Bun"; import * as HttpClient from "effect/unstable/http/HttpClient"; import Stack from "../alchemy.run.ts"; const stack = beforeAll(deploy(Stack, { stage: `pr-${Date.now()}` })); afterAll.skipIf(!process.env.CI)(destroy(Stack)); test("PUT + GET round-trips through R2", Effect.gen(function* () { const { url } = yield* stack; const res = yield* HttpClient.get(`${url}/object/hello.txt`); expect(yield* res.text).toBe("hi!"); }));
$
Every PR is a preview. Auto-destroyed on merge.
Open a PR — alchemy deploys an isolated stage, comments the URL on the PR, and tears it down on merge. One workflow file. Every environment. Learn more →
- 1PR opened
- 2Deploy
- 3Comment
- 4Merged & destroyed
$# pull_request opened — STAGE=pr-147# workflow queued…
# .github/workflows/deploy.yml env: STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || (github.ref == 'refs/heads/main' && 'prod' || github.ref_name) }}
pr-{n} for PRs, prod for main, branch name otherwise — one expression, no scripting.prod. Even if you manage to trigger it on the wrong event.Observability is just more infrastructure.
Effect already emits OpenTelemetry by default. Alchemy declares the exporter as a Layer — point it at Axiom, Datadog, CloudWatch, or any OTLP endpoint — and you ship the dashboard with the service. Alarms live next to the metrics they watch, in the same alchemy.run.ts. Learn more →
// Effect emits OpenTelemetry by default. // Pick an exporter Layer; the Worker code never changes. export default class Api extends Cloudflare.Worker<Api>()( "Api", Effect.gen(function* () { yield* Effect.logInfo("request received"); yield* Metric.increment(requestsTotal); return { fetch: handler }; }).pipe( Effect.provide(AxiomExporter), // or CloudWatch, Datadog, OTLP … ), ) {}
// alchemy.run.ts — same program. operations included. export const Dashboard = AWS.CloudWatch.Dashboard("ApiHealth", { widgets: [ Widget.line({ title: "p99 latency", metric: api.metrics.p99 }), Widget.line({ title: "requests / sec", metric: api.metrics.rps }), Widget.number({ title: "5xx ratio", metric: api.metrics.errorRate }), ], }); export const P99Alarm = AWS.CloudWatch.Alarm("p99Latency", { metric: api.metrics.p99, threshold: 500, comparisonOperator: ">", evaluationPeriods: 5, alarmActions: [pagerDuty, slackWebhook], });