Resource
A Resource represents a cloud entity managed by Alchemy — a bucket, database, queue, function, DNS record, or anything else that has a lifecycle of reconcile and delete.
Declaring a Resource
Section titled “Declaring a Resource”Resources are declared with a logical ID and optional input properties:
const bucket = yield* Cloudflare.R2Bucket("Bucket");const queue = yield* AWS.SQS.Queue("Jobs", { fifoQueue: true,});The logical ID ("Bucket", "Jobs") is stable across deploys. It
identifies this resource within the stack and is used to track state.
Input Properties and Output Attributes
Section titled “Input Properties and Output Attributes”Every resource has two sides:
- Input Properties — the desired configuration you pass in
(e.g.
fifoQueue: true) - Output Attributes — the values produced after creation
(e.g.
queueUrl,queueArn)
Output attributes are available as Output
expressions on the resource — lazy, typed references that resolve
once the upstream resource has been created:
const bucket = yield* Cloudflare.R2Bucket("Bucket");bucket.bucketName; // Output<string>See Inputs and Outputs for the full set of
operators (map, mapEffect, all, interpolate, ref).
These are lazy references that resolve after the resource is created. You can pass them as inputs to other resources to express dependencies.
Resources are Effects
Section titled “Resources are Effects”A resource declaration like Cloudflare.R2Bucket("Bucket") is just
an Effect — calling it doesn’t talk to the cloud. yield*-ing it
inside a Stack doesn’t either; it just registers
the resource on the stack and hands you back a typed
Output reference for its attributes:
// 1. Build the Effect. No API calls. No state mutation.const Bucket = Cloudflare.R2Bucket("Bucket");
// 2. Register it on the stack. Still no API calls — alchemy is// just collecting the desired-state graph.export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers() }, Effect.gen(function* () { const bucket = yield* Bucket; return { name: bucket.bucketName }; }),);The cloud is only touched later, when alchemy deploy runs the
collected graph through plan and apply. See
Resource Lifecycle for what happens
after registration.
Sharing across files
Section titled “Sharing across files”Because the declaration is just a value, you can export it and
import it from anywhere — handlers, layers, other resources:
export const Bucket = Cloudflare.R2Bucket("Bucket");import { Bucket } from "./src/bucket.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers() }, Effect.gen(function* () { yield* Bucket; }),);Importing the same Bucket from multiple files is safe. Alchemy
keys resources by their fully qualified name, so even if two
modules yield* it, it registers on the stack exactly once.
Logical ID
Section titled “Logical ID”The first argument you pass to a resource constructor is its logical ID — a name you choose to identify the resource within its stack:
const Bucket = Cloudflare.R2Bucket("Bucket"); // logical ID: "Bucket"const Jobs = AWS.SQS.Queue("Jobs"); // logical ID: "Jobs"The logical ID is how alchemy tracks the resource in state across deploys:
- Stable across deploys — keep the same ID and alchemy keeps updating the same underlying cloud resource.
- Stable across renames — change the variable name, change the TypeScript class, move the file; as long as the logical ID stays the same, alchemy still recognizes it.
- Rename = replace — change the logical ID and alchemy treats it as a new resource (and deletes the old one on the next deploy).
Logical IDs only need to be unique within a stack.
Physical Name
Section titled “Physical Name”The physical name is what the cloud actually sees — myapp-dev_sam-bucket-a3f1
on R2, an ARN suffix on AWS, etc. Alchemy generates it for you from
three things:
{stack-name}-{stage}-{logical-id}-{instance-id} "myapp" "dev_sam" "Bucket" "a3f1"The first three are obvious. The instance ID is a short, deterministic suffix tied to this specific instance of the resource. While the resource lives, the instance ID stays the same, so re-running create finds the existing resource instead of duplicating it.
The whole scheme means:
- Stages don’t collide —
dev_samandprodproduce different physical names from the same code. - Creates are idempotent — same logical ID + same instance ID = same physical name on retry.
- State can recover — if persistence fails, alchemy can re-run create and find the existing cloud resource.
The instance ID is the part that does change when a resource is replaced — which leads us to…
Replacement
Section titled “Replacement”Some property changes can’t be applied in place. Changing a DynamoDB table’s partition key, for example, can’t be done on a live table — it has to be re-created.
Before:
const Jobs = DynamoDB.Table("Jobs", { partitionKey: "id", attributes: { id: "S" },});After:
const Jobs = DynamoDB.Table("Jobs", { partitionKey: "id", attributes: { id: "S" }, partitionKey: "tenantId", attributes: { tenantId: "S" },});The logical ID ("Jobs") doesn’t change, but the instance ID
does — which means the physical name does too:
before: myapp-prod-jobs-a3f1after: myapp-prod-jobs-9b2cWhen the next plan runs, alchemy:
- Creates a new table with the new instance ID (and physical name)
- Updates downstream resources to reference the new one
- Deletes the old table
The resource’s provider decides which property
changes trigger replacement vs in-place update (via
diff). For the full lifecycle
(reconcile / replace / delete) see
Resource Lifecycle.
Defining your own Resource type
Section titled “Defining your own Resource type”A resource is just a typed Effect. To support a new cloud or
third-party API, declare a Resource type with its input props and
output attributes — then implement its provider as a Layer. Same
engine plans, deploys, and destroys it.
See Writing a Custom Resource Provider
for a step-by-step walkthrough of declaring the type and
implementing each lifecycle hook (reconcile, delete, diff,
read).
// 1. Declare the type + constructor.export type StripeProduct = Resource< "Stripe.Product", { name: string; price: number }, // input props { productId: string; priceId: string } // output attrs>;export const StripeProduct = Resource<StripeProduct>("Stripe.Product");
// 2. Use it like any built-in resource.const Pro = yield* StripeProduct("Pro", { name: "Pro plan", price: 2900,});// ^? typed Pro.productId, Pro.priceId- Inputs & outputs are typed — Props you pass in, attributes the provider returns. Both fully typed, both checked at the call site.
- Compose with built-in providers — Merge your provider Layer with
Cloudflare.providers()orAWS.providers(). One stack, mixed clouds.
The lifecycle hooks the provider implements — reconcile,
delete, diff, read — are documented in
Provider.
The resource graph
Section titled “The resource graph”Passing an Output from one resource as input to another draws an
edge in the dependency graph. Take this stack:
const Bucket = yield* Cloudflare.R2Bucket("Bucket");const Sessions = yield* Cloudflare.KVNamespace("Sessions");
const Queue = yield* AWS.SQS.Queue("Queue", { name: Output.interpolate`${Bucket.bucketName}-events`,});
const Worker = yield* Cloudflare.Worker("Worker", { main: import.meta.path, bindings: { Bucket, Sessions, Queue },});Alchemy reads the Outputs in each resource’s props and builds:
It then deploys in topological order:
BucketandSessionshave no dependencies → created in parallel.Queuedepends onBucket.bucketName→ waits forBucket, then created.Workerdepends on all three → created last, after every upstream Output has resolved.
Cycles (Worker A binds Worker B, Worker B binds Worker A) are handled with a two-phase plan — see Circular Bindings.
Circular references
Section titled “Circular references”Real systems have cycles. Two Workers that call each other. A Lambda that invokes another Lambda. Tables that reference each other. Most IaC engines reject these — alchemy resolves them by splitting each Platform resource into two pieces:
- A class that acts as the Tag (the identity / declaration)
- A
.make(...)Layer that supplies the runtime implementation
The class can be referenced before its implementation exists, so two Workers can name each other in their handlers without a hard ordering constraint.
For a circular Worker pair, the resource graph contains edges in both directions:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
// The class is the Tag — used as a typed identifier elsewhere.export class MyWorker extends Cloudflare.Worker<MyWorker>()("MyWorker", { main: import.meta.path,}) {}
// The default export is the Layer — the runtime implementation.export default MyWorker.make( Effect.gen(function* () { return { fetch: Effect.gen(function* () { return HttpServerResponse.text("hello"); }), }; }),);To compose Workers that reference each other, provide both Layers
to your Stack with Effect.provide. See the
Circular Bindings guide for a complete
worked A↔B example, including how alchemy plans the two-phase
create-then-wire deploy.