Provider
A Provider implements the lifecycle operations for a resource
type. When you yield* a resource inside a Stack, alchemy looks up
the provider for that resource’s type and calls the appropriate
lifecycle method. This is what gives meaning to a
Resource declaration — the engine plans, the
provider acts.
Provider Layers
Section titled “Provider Layers”Providers are registered as Effect Layers. Cloudflare.providers()
and AWS.providers() return Layers bundling every built-in provider
for that cloud:
Alchemy.Stack( "MyApp", { providers: Cloudflare.providers() }, Effect.gen(function* () { yield* Cloudflare.R2Bucket("Bucket"); }),);The type system enforces this — using a Cloudflare resource without
Cloudflare.providers() raises a compile-time error.
To mix clouds, merge the layers:
import * as Layer from "effect/Layer";
Alchemy.Stack( "MyApp", { providers: Layer.mergeAll(Cloudflare.providers(), AWS.providers()), }, Effect.gen(function* () { yield* Cloudflare.R2Bucket("Bucket"); yield* AWS.SQS.Queue("Jobs"); }),);Lifecycle operations
Section titled “Lifecycle operations”A provider is an object implementing some subset of these operations.
reconcile and delete are required; the rest are optional hooks
alchemy uses for richer behavior.
reconcile
Section titled “reconcile”Required. Called for every “make it so” intent — first-time provisioning, routine updates, and adoption takeovers. Returns the output attributes that downstream resources can reference.
A reconciler is a single observe → ensure → sync → return flow:
reconcile: Effect.fnUntraced(function* ({ news, output }) { const stripe = yield* StripeClient;
// Observe — fetch live state if we have a cached id. let product = output?.productId ? yield* Effect.tryPromise(() => stripe.products.retrieve(output.productId), ).pipe(Effect.catchAll(() => Effect.succeed(undefined))) : undefined;
// Ensure — create if missing. if (!product) { product = yield* Effect.tryPromise(() => stripe.products.create({ name: news.name }), ); }
// Sync — patch any field that drifted from desired. if (product.name !== news.name) { product = yield* Effect.tryPromise(() => stripe.products.update(product!.id, { name: news.name }), ); }
return { productId: product.id, name: product.name };}),The provider receives output: Attributes | undefined and
olds: Props | undefined:
output | olds | Meaning |
|---|---|---|
undefined | undefined | Greenfield — no prior resource |
| defined | defined | Routine update |
| defined | undefined | Adoption — engine adopted via read |
The reconciler must work for all three combinations. Do not
branch the body on output === undefined — that just renames the
old create/update split. Trust observed cloud state, not olds.
reconcile must be idempotent: alchemy may retry it after a
state persistence failure. Deterministic physical names plus the
observe step ensure a retry finds the existing resource and re-syncs
any drifted fields.
See the custom provider guide for the full walkthrough.
delete
Section titled “delete”Required. Called when a resource is removed from code, replaced, or
when running alchemy destroy. Like create, must be idempotent
— treat “already deleted” as success.
delete: ({ output }) => Effect.promise(() => stripe.products.del(output.productId));Optional. Called during planning to decide what kind of change is
needed when properties differ. Returns a tagged Diff describing
the action:
diff: Effect.fnUntraced(function* ({ news, olds }) { if (news.region !== olds.region) { return { action: "replace" } as const; } if (news.name !== olds.name) { return { action: "update" } as const; } return { action: "noop" } as const;}),The shape is one of:
{ action: "noop" }— properties differ trivially; don’t callupdate.{ action: "update", stables?: [...] }— apply an in-place update.stableslists props that are guaranteed not to change during this update.{ action: "replace", deleteFirst?: boolean }— destroy and recreate (see Resource Lifecycle › Replace). SetdeleteFirst: truefor resources that can’t coexist with their replacement (e.g. unique-name constraints).void/undefined— fall back to the default behavior (treat the change as anupdate).
A provider can also declare top-level stables for attributes that
are immutable across all updates (e.g. ARNs, resource IDs).
For comparing nested objects, use the deepEqual and
anyPropsAreDifferent helpers from alchemy/Diff. If any input
might still be unresolved when diff runs (an unresolved Output),
guard with isResolved first and return undefined to let the
default path handle it.
Optional. The engine consults read whenever a resource has no prior
state — both for state recovery (state lost between create and
persist) and for adoption (a fresh state store deploying against
existing cloud infrastructure). It is the single source of truth for
“does this resource exist, and is it ours?”.
read: Effect.fnUntraced(function* ({ id, olds, output }) { const stripe = yield* StripeClient; if (!output?.productId) return undefined; const product = yield* Effect.tryPromise(() => stripe.products.retrieve(output.productId), ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); if (!product) return undefined; return { productId: product.id, name: product.name };}),read returns one of three values:
undefined— the resource doesn’t exist. The engine will drive a normalcreate.- plain attributes — the resource exists and we own it.
The engine silently adopts: it persists the attributes as the
initial
createdstate and lets ordinarydiffdecide whether the next deploy is a noop or update. Unowned(attributes)— the resource exists but we don’t own it. By default the engine fails withOwnedBySomeoneElseso you don’t accidentally clobber a production resource. Re-running with--adopt(or scoping the effect withadopt(true)) unlocks a takeover.
Unowned is a brand from alchemy/AdoptPolicy — there’s no wrapper
to unwrap, just a hidden symbol on the attributes object:
import { Unowned } from "alchemy/AdoptPolicy";
read: Effect.fnUntraced(function* ({ id, olds }) { const live = yield* lookup(olds.name); if (!live) return undefined; const attrs = { productId: live.id, name: live.name }; return ownsResource(id, live.tags) ? attrs : Unowned(attrs);}),Inputs. read may be called for an existence/adoption probe with
output: undefined (no prior state). Resources whose live lookup
requires a previously-persisted ID (e.g. output.productId) should
return undefined in that case — they have no way to find the
resource without it.
Ownership detection. Resources with no notion of ownership (e.g.
“the cloud API just gives us a single global object by name”) should
always return plain attributes. The engine treats them as owned and
silent adoption is the default, so --adopt is unnecessary.
Resources with tag-based or naming-based ownership (most cloud
resources) should brand foreign-owned attributes with Unowned.
Lifecycle methods don’t gate on ownership. Once
readclears a resource for write,reconcilecan assume it’s safe to proceed. Don’t repeat the ownership check insidereconcile— that just hides the policy decision from the engine.
precreate
Section titled “precreate”Optional. Reserves a physical name (or stub resource) before
create runs. This is what enables circular
bindings — Worker A can know Worker B’s
URL before either is fully created.
tail / logs
Section titled “tail / logs”Optional. Power alchemy tail (live log streaming) and
alchemy logs (historical fetch). Each returns a Stream or
Effect of LogLine values respectively.
Plan and apply
Section titled “Plan and apply”Alchemy combines every provider’s lifecycle operations into a single plan, then applies it.
- Plan — for each declared resource, alchemy compares the
desired props against persisted state and uses
diffto decide whether it’s acreate,update,replace, ornoop. Anything in state but no longer declared is marked fordelete. - Apply — alchemy walks the plan in dependency order and calls
the matching lifecycle operation on each provider, surfacing the
results as
Outputs for downstream resources.
See Resource Lifecycle for how a resource moves through these phases over time.
Authoring a custom provider
Section titled “Authoring a custom provider”To add support for a new cloud or third-party API, declare a
Resource type and implement its provider as
an Effect Layer. Because providers are just Layers, you can
merge them with Cloudflare.providers() or AWS.providers() —
one stack, mixed clouds, no codegen.
See Writing a Custom Resource Provider
for a step-by-step walkthrough that builds a Stripe Product
provider end-to-end (props, attributes, resource constructor,
provider Layer, providers() bundle, and tests).
For the lifecycle semantics that govern when each operation fires, see Resource Lifecycle.