Infrastructure that feels like importing a module and calling a function. Alchemy is a TypeScript-native Infrastructure-as-Code library — no YAML, no HCL, no separate DSL. Resources are plain async functions you await. In this tutorial you will build and deploy a complete Cloudflare Workers API backed by a D1 database, a KV namespace, an R2 bucket, and typed secrets — all from a single alchemy.run.ts file.
What You Will Learn
By the end of this tutorial, you will:
- Understand what Alchemy is and how it differs from Terraform, Pulumi, SST, and Wrangler
- Scaffold an Alchemy project and authenticate against Cloudflare
- Deploy a Cloudflare Worker written in pure TypeScript
- Provision and bind a D1 database, a KV namespace, and an R2 bucket
- Manage secrets safely with
alchemy.secret - Run a local development loop that emulates your bindings
- Inspect and understand state files and what
finalize()does - Tear everything down cleanly with one command
Prerequisites
Before starting, ensure you have:
- Node.js 20+ or Bun 1.1+ installed (
node --version) - A free Cloudflare account (no paid plan required for this tutorial)
- Basic familiarity with TypeScript and async/await
- A code editor (VS Code recommended)
No prior Terraform, Pulumi, or Wrangler experience is needed. If anything, forgetting them will help.
What Is Alchemy?
Alchemy is an embeddable, zero-dependency, TypeScript-native Infrastructure-as-Code (IaC) library. Where most IaC tools ask you to learn a configuration language — Terraform's HCL, CloudFormation's YAML, Pulumi's SDK abstractions — Alchemy asks you to learn nothing new. A resource is just an async function:
const bucket = await R2Bucket("uploads");That single line creates an R2 bucket if it does not exist, updates it if its configuration changed, and records its state locally. There is no plan/apply ceremony, no provider plugins to install, and no hidden control plane. The mental model is simply: call a function, get a resource back.
How it compares
| Tool | Language | Mental model | State |
|---|---|---|---|
| Terraform | HCL | Declarative graph | Remote/local .tfstate |
| Pulumi | TS/Go/Python SDK | Declarative graph via SDK | Pulumi service |
| SST | TS over Pulumi engine | Constructs | Managed/local |
| Wrangler | wrangler.toml | CLI + config file | Cloudflare-managed |
| Alchemy | Plain TypeScript | Async functions | Local files you can commit |
The key differences that make Alchemy stand out in 2026:
- Resources are async functions. No new abstraction layer to learn.
- Pure ESM TypeScript. It runs anywhere modern JavaScript runs, with first-class support for Bun.
- State lives in your repo. State files are stored locally, are human-readable, and can be inspected, edited, and committed.
- Multi-provider. Cloudflare and AWS are first-class, and you can generate a provider for any REST API quickly.
This tutorial focuses on Cloudflare because it is the fastest path from zero to a deployed, stateful API.
Step 1: Scaffold the Project
The quickest way to start is the create command, which sets up a TypeScript project, installs Alchemy, and wires up the scripts:
# With npm
npx alchemy@latest create alchemy-api --template typescript
# With Bun (recommended — Alchemy loves Bun)
bunx alchemy@latest create alchemy-api --template typescriptMove into the directory:
cd alchemy-apiThe template gives you a structure like this:
alchemy-api/
├── alchemy.run.ts # Your infrastructure — the heart of the project
├── src/
│ └── worker.ts # Your Worker's application code
├── package.json
├── tsconfig.json
└── .env # Local secrets (gitignored)The two files that matter are alchemy.run.ts (what to deploy) and src/worker.ts (the code that runs). That separation — infrastructure and application living side by side in the same language — is the whole point.
Step 2: Authenticate with Cloudflare
Alchemy needs permission to manage resources in your Cloudflare account. Run the interactive login once:
npx alchemy loginThis performs an OAuth flow in your browser and stores a token locally. For CI environments, you can skip the interactive login and provide a CLOUDFLARE_API_TOKEN environment variable instead — Alchemy will pick it up automatically.
If you ever need to reconfigure project settings, run:
npx alchemy configureStep 3: Deploy Your First Worker
Open alchemy.run.ts. At its simplest, the file initializes an app, declares resources, and finalizes. Replace the contents with:
import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";
// Initialize the app. The name namespaces all resources and state.
const app = await alchemy("alchemy-api");
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
});
console.log(`Worker deployed at: ${worker.url}`);
// Finalize commits state and removes orphaned resources.
await app.finalize();Now write the actual Worker in src/worker.ts:
export default {
async fetch(request: Request): Promise<Response> {
return Response.json({
message: "Hello from Alchemy!",
timestamp: new Date().toISOString(),
});
},
};Deploy it:
npx alchemy deployAlchemy creates the Worker, uploads your bundled code, and prints a live *.workers.dev URL. Open it — you should see your JSON response. You just shipped a serverless API with zero config files.
What just happened?
alchemy("alchemy-api")opened a "scope" that tracks every resource you create. Eachawait Worker(...)call reconciled desired state with reality.app.finalize()then wrote the state file and deleted anything that exists in state but is no longer declared in code. This is how Alchemy garbage-collects removed resources.
Step 4: Add a D1 Database
A real API needs persistence. Cloudflare D1 is a serverless SQLite database, and Alchemy provisions it like any other resource. Update alchemy.run.ts:
import alchemy from "alchemy";
import { Worker, D1Database } from "alchemy/cloudflare";
const app = await alchemy("alchemy-api");
// Provision a D1 database.
const db = await D1Database("app-db", {
name: "alchemy-app-db",
});
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
// Bindings expose resources to your Worker by name.
bindings: {
DB: db,
},
});
console.log(`Worker deployed at: ${worker.url}`);
await app.finalize();The bindings object is the bridge between infrastructure and runtime. The key DB becomes available inside your Worker as env.DB, fully typed as a D1 client. No wrangler.toml, no manual binding declarations.
Typing your Worker environment
Alchemy infers the binding types from your alchemy.run.ts. Export the worker so its environment type flows into your application code. Add an export in alchemy.run.ts:
export { worker };Then in src/worker.ts, import the inferred environment type:
import type { worker } from "../alchemy.run.ts";
type Env = typeof worker.Env;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/users") {
// env.DB is a fully typed D1 client — no manual type wiring.
const { results } = await env.DB.prepare(
"SELECT id, name FROM users LIMIT 10",
).all();
return Response.json({ users: results });
}
return Response.json({ message: "Hello from Alchemy!" });
},
};The binding name in alchemy.run.ts and the property on env are guaranteed to match because they come from the same source of truth. Rename a binding and TypeScript flags every usage.
Step 5: Add KV and R2 Bindings
Most apps need more than a database — a cache and object storage are common. Alchemy treats KV namespaces and R2 buckets exactly like D1: declare, then bind.
import alchemy from "alchemy";
import {
Worker,
D1Database,
KVNamespace,
R2Bucket,
} from "alchemy/cloudflare";
const app = await alchemy("alchemy-api");
const db = await D1Database("app-db", { name: "alchemy-app-db" });
const cache = await KVNamespace("app-cache");
const uploads = await R2Bucket("app-uploads");
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
bindings: {
DB: db,
CACHE: cache,
UPLOADS: uploads,
},
});
console.log(`Worker deployed at: ${worker.url}`);
export { worker };
await app.finalize();Now all three resources are available inside the Worker as env.CACHE and env.UPLOADS, again fully typed:
import type { worker } from "../alchemy.run.ts";
type Env = typeof worker.Env;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// KV: read-through cache example
if (url.pathname === "/config") {
const cached = await env.CACHE.get("config");
if (cached) return new Response(cached);
const fresh = JSON.stringify({ theme: "dark", version: 2 });
// Cache for one hour
await env.CACHE.put("config", fresh, { expirationTtl: 3600 });
return new Response(fresh);
}
// R2: store an uploaded file
if (url.pathname === "/upload" && request.method === "POST") {
const key = `uploads/${Date.now()}`;
await env.UPLOADS.put(key, request.body);
return Response.json({ stored: key });
}
return Response.json({ message: "Hello from Alchemy!" });
},
};Redeploy with npx alchemy deploy. Alchemy diffs your declared resources against the recorded state: the database already exists, so it is left alone, while the new KV namespace and R2 bucket are created and bound. This incremental reconciliation is what makes IaC safe to run repeatedly.
Step 6: Manage Secrets Safely
API keys and tokens must never sit in plaintext in your code or state files. Alchemy provides alchemy.secret, which encrypts values before they are written to state and injects them into the Worker at runtime.
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
bindings: {
DB: db,
CACHE: cache,
UPLOADS: uploads,
// Wrap any sensitive value so it is encrypted at rest in state.
API_KEY: alchemy.secret(process.env.STRIPE_KEY),
},
});You can also pull directly from environment variables with the .env helper, which reads the named variable and fails loudly if it is missing:
bindings: {
// Reads process.env.STRIPE_KEY, encrypts it, errors if unset.
API_KEY: alchemy.secret.env.STRIPE_KEY,
},Inside the Worker, the secret is a plain string on env:
const auth = request.headers.get("authorization");
if (auth !== `Bearer ${env.API_KEY}`) {
return new Response("Unauthorized", { status: 401 });
}Store the actual value in your local .env file (which the template gitignores) and in your CI secret manager for deployments. The encrypted form is what lands in state, so committing state never leaks credentials.
Tip: Alchemy encrypts secrets in state using a passphrase. Set
ALCHEMY_PASSWORDin your environment for both local and CI runs so the same state can be decrypted across machines.
Step 7: The Local Development Loop
Deploying to Cloudflare for every change is slow. Alchemy ships a development mode that emulates your bindings locally with hot reloading:
npx alchemy devMost Cloudflare bindings — D1, KV, and R2 — are emulated automatically, so env.DB, env.CACHE, and env.UPLOADS work without touching your production resources. Secrets resolve from your local .env. Edit src/worker.ts, save, and the Worker reloads instantly against the local emulation.
This gives you the tight feedback loop of a local server while keeping a single source of truth: the same alchemy.run.ts describes both your local dev environment and production. There is no drift between a wrangler.toml for dev and something else for prod.
Step 8: Understand State Files
After your first deploy, look in the .alchemy/ directory. You will find JSON state files describing every resource Alchemy manages — IDs, configuration, and (encrypted) secrets. This is deliberately not hidden behind a remote service.
Because state is local and human-readable, you can:
- Commit it to version control so your team shares a consistent view of deployed infrastructure
- Inspect it to understand exactly what exists and why
- Diff it in pull requests to see how a change affects real resources
This is a different philosophy from Terraform Cloud or Pulumi's managed backend. Alchemy trusts you with your own state. For teams, you can configure a shared state store (for example, backed by a Cloudflare resource) so concurrent deploys stay consistent — but for solo projects and small teams, committed local state is often all you need.
Step 9: Tear It All Down
One of the best tests of an IaC tool is how cleanly it removes what it made. Alchemy tracks every resource in state, so a single command destroys all of them in dependency order:
npx alchemy destroyThis deletes the Worker, the D1 database, the KV namespace, and the R2 bucket — everything in your app's scope. No orphaned resources quietly accruing cost on your Cloudflare bill. Because destruction is driven by state, it is exact: only what Alchemy created is removed.
Testing Your Implementation
Verify the full flow end to end:
- Deploy: run
npx alchemy deployand confirm the printed URL responds. - Database:
curl https://your-worker.workers.dev/usersreturns a JSON array (empty until you seed data). - Cache:
curl https://your-worker.workers.dev/configtwice — the second call is served from KV. - Storage:
curl -X POST --data-binary @file.png https://your-worker.workers.dev/uploadreturns a stored key. - Secret: a request without the correct
Authorizationheader to a protected route returns401. - Dev mode: run
npx alchemy dev, edit the Worker, and confirm hot reload works against emulated bindings. - Teardown:
npx alchemy destroyremoves everything; re-running it is a no-op.
Troubleshooting
Not authenticated on deploy. Run npx alchemy login again, or set CLOUDFLARE_API_TOKEN in CI. Verify the token has Workers, D1, KV, and R2 permissions.
Binding is undefined at runtime. The key in the bindings object must match the property you read on env. Make sure you exported worker from alchemy.run.ts and imported typeof worker.Env so TypeScript catches mismatches.
Secret cannot be decrypted across machines. Set the same ALCHEMY_PASSWORD everywhere the state is used. A different passphrase cannot decrypt secrets written with another.
State conflicts when deploying from two machines. Local state is per-checkout. For shared deploys, configure a remote state store rather than committing concurrent edits to the same JSON files.
destroy leaves a resource behind. This usually means the resource was created outside Alchemy (for example, manually in the dashboard). Alchemy only manages what is in its state.
Next Steps
- Add Durable Objects and Queues — both are first-class Cloudflare resources in
alchemy/cloudflare, bound exactly like D1 and KV. - Wire Alchemy into CI/CD with a GitHub Actions workflow that runs
alchemy deployon merge — see our GitHub Actions CI/CD guide. - Compare against the SST approach in our SST Ion AWS deployment tutorial.
- Deploy a full app on Workers with our Cloudflare Workers + Hono + D1 serverless API tutorial.
- Generate a custom provider for an internal REST API and manage it alongside your Cloudflare resources.
Conclusion
Alchemy collapses the gap between application code and infrastructure by refusing to introduce a new language for the latter. Resources are async functions, bindings are typed objects, state is readable JSON you own, and the same file describes both local development and production. In this tutorial you scaffolded a project, deployed a Cloudflare Worker, attached a D1 database, a KV namespace, and an R2 bucket, secured it with encrypted secrets, ran a local dev loop, and tore the whole thing down with one command.
For teams already living in TypeScript — especially on Cloudflare — Alchemy removes an entire category of context-switching. There is no YAML to remember, no HCL to learn, and no managed backend to depend on. Just code that builds, deploys, and cleans up the infrastructure your application needs.