1---
2title: Providers
3description: Providers implement the lifecycle operations for a resource type — reconcile, delete, diff, read, and more.
4---
5
6A **Provider** implements the lifecycle operations for a resource
7type. When you `yield*` a resource inside a Stack, alchemy looks up
8the provider for that resource's type and calls the appropriate
9lifecycle method. This is what gives meaning to a
10[Resource](/infrastructure-as-code/resource) declaration — the engine plans, the
11provider acts.
12
13## Provider layers
14
15Providers are registered as Effect Layers. `Cloudflare.providers()`
16and `AWS.providers()` return Layers bundling every built-in provider
17for that cloud:
18
19```typescript
20Alchemy.Stack(
21 "MyApp",
22 { providers: Cloudflare.providers(), state: Cloudflare.state() },
23 Effect.gen(function* () {
24 yield* Cloudflare.R2.Bucket("Bucket");
25 }),
26);
27```
28
29The type system enforces this — using a Cloudflare resource without
30`Cloudflare.providers()` raises a compile-time error.
31
32## Multiple providers
33
34A stack can use any number of providers. Because providers are
35Layers, combining them is just `Layer.mergeAll`:
36
37```typescript
38import * as Layer from "effect/Layer";
39
40Alchemy.Stack(
41 "MyApp",
42 {
43 providers: Layer.mergeAll(Cloudflare.providers(), AWS.providers()),
44 },
45 Effect.gen(function* () {
46 yield* Cloudflare.R2.Bucket("Bucket");
47 yield* AWS.SQS.Queue("Jobs");
48 }),
49);
50```
51
52This works for as many providers as you need — built-in ones
53(`Cloudflare`, `AWS`, `Neon`, `Planetscale`, `Docker`, …) and
54[custom providers](/infrastructure-as-code/custom-provider) merge
55the same way:
56
57```typescript
58providers: Layer.mergeAll(
59 Cloudflare.providers(),
60 AWS.providers(),
61 Neon.providers(),
62 StripeProviders, // your own custom provider layer
63),
64```
65
66The type system tracks the requirements per resource: each resource
67you `yield*` demands its own provider from the merged layer, so a
68missing provider is a compile-time error, never a deploy-time
69surprise.
70
71## Lifecycle operations
72
73A provider is an object implementing some subset of these operations.
74`reconcile` and `delete` are required; the rest are optional hooks
75alchemy uses for richer behavior.
76
77### `reconcile`
78
79Required. Called for every "make it so" intent — first-time
80provisioning, routine updates, and adoption takeovers. Returns the
81**output attributes** that downstream resources can reference.
82
83A reconciler is a single observe → ensure → sync → return flow:
84
85```typescript
86reconcile: Effect.fn(function* ({ news, output }) {
87 let live = yield* observe(output); // read live cloud state
88 if (!live) live = yield* create(news); // ensure it exists
89 if (drifted(live, news)) {
90 live = yield* update(live.id, news); // sync only the delta
91 }
92 return toAttributes(live); // fresh attributes
93}),
94```
95
96The provider receives `output: Attributes | undefined` and
97`olds: Props | undefined`:
98
99| `output` | `olds` | Meaning |
100| ----------- | ----------- | ------------------------------------ |
101| `undefined` | `undefined` | Greenfield — no prior resource |
102| defined | defined | Routine update |
103| defined | `undefined` | Adoption — engine adopted via `read` |
104
105The reconciler must work for all three combinations. **Do not
106branch the body on `output === undefined`** — that just renames the
107old `create`/`update` split. Trust observed cloud state, not `olds`.
108
109`reconcile` must be **idempotent**: alchemy may retry it after a
110state persistence failure. Deterministic physical names plus the
111observe step ensure a retry finds the existing resource and re-syncs
112any drifted fields.
113
114For a real reconciler built up step by step — observe, ensure,
115sync, return, against the Stripe API — see the
116[custom provider guide](/infrastructure-as-code/custom-provider#implement-reconcile).
117
118### `delete`
119
120Required. Called when a resource is removed from code, replaced, or
121when running `alchemy destroy`. Like `reconcile`, must be **idempotent**
122— treat "already deleted" as success.
123
124```typescript
125delete: ({ output }) =>
126 Effect.promise(() => stripe.products.del(output.productId));
127```
128
129### `diff`
130
131Optional. Called during planning to decide what kind of change is
132needed when properties differ. Returns a tagged `Diff` describing
133the action:
134
135```typescript
136diff: Effect.fn(function* ({ news, olds }) {
137 if (news.region !== olds.region) {
138 return { action: "replace" } as const;
139 }
140 if (news.name !== olds.name) {
141 return { action: "update" } as const;
142 }
143 return { action: "noop" } as const;
144}),
145```
146
147The shape is one of:
148
149- **`{ action: "noop" }`** — properties differ trivially; don't
150 call `update`.
151- **`{ action: "update", stables?: [...] }`** — apply an in-place
152 update. `stables` lists props that are guaranteed not to change
153 during this update.
154- **`{ action: "replace", deleteFirst?: boolean }`** — destroy and
155 recreate (see [Resource Lifecycle › Replace](/infrastructure-as-code/resource-lifecycle#replace)).
156 Set `deleteFirst: true` for resources that can't coexist with
157 their replacement (e.g. unique-name constraints).
158- **`void` / `undefined`** — fall back to the default behavior
159 (treat the change as an `update`).
160
161A provider can also declare top-level `stables` for attributes that
162are immutable across all updates (e.g. ARNs, resource IDs).
163
164For comparing nested objects, use the `deepEqual` and
165`anyPropsAreDifferent` helpers from `alchemy/Diff`. If any input
166might still be unresolved when `diff` runs (an unresolved `Output`),
167guard with `isResolved` first and return `undefined` to let the
168default path handle it.
169
170### `read`
171
172Optional. The engine consults `read` whenever a resource has no prior
173state — both for **state recovery** (state lost between create and
174persist) and for **adoption** (a fresh state store deploying against
175existing cloud infrastructure). It is the single source of truth for
176"does this resource exist, and is it ours?".
177
178```typescript
179read: Effect.fn(function* ({ id, olds, output }) {
180 const stripe = yield* StripeClient;
181 if (!output?.productId) return undefined;
182 const product = yield* Effect.tryPromise(() =>
183 stripe.products.retrieve(output.productId),
184 ).pipe(Effect.catchAll(() => Effect.succeed(undefined)));
185 if (!product) return undefined;
186 return { productId: product.id, name: product.name };
187}),
188```
189
190`read` returns one of three values:
191
192- **`undefined`** — the resource doesn't exist. The engine will
193 drive a normal `create`.
194- **plain attributes** — the resource exists and we own it.
195 The engine silently adopts: it persists the attributes as the
196 initial `created` state and lets ordinary `diff` decide whether
197 the next deploy is a noop or update.
198- **`Unowned(attributes)`** — the resource exists but we *don't*
199 own it. By default the engine fails with `OwnedBySomeoneElse`
200 so you don't accidentally clobber a production resource.
201 Re-running with `--adopt` (or scoping the effect with
202 `adopt(true)`) unlocks a takeover.
203
204`Unowned` is a brand from `alchemy/AdoptPolicy` — there's no wrapper
205to unwrap, just a hidden symbol on the attributes object:
206
207```typescript
208import { Unowned } from "alchemy/AdoptPolicy";
209
210read: Effect.fn(function* ({ id, olds }) {
211 const live = yield* lookup(olds.name);
212 if (!live) return undefined;
213 const attrs = { productId: live.id, name: live.name };
214 return ownsResource(id, live.tags) ? attrs : Unowned(attrs);
215}),
216```
217
218**Inputs.** `read` may be called for an existence/adoption probe with
219`output: undefined` (no prior state). Resources whose live lookup
220requires a previously-persisted ID (e.g. `output.productId`) should
221return `undefined` in that case — they have no way to find the
222resource without it.
223
224**Ownership detection.** Resources with no notion of ownership (e.g.
225"the cloud API just gives us a single global object by name") should
226always return plain attributes. The engine treats them as owned and
227silent adoption is the default, so `--adopt` is unnecessary.
228Resources with tag-based or naming-based ownership (most cloud
229resources) should brand foreign-owned attributes with `Unowned`.
230
231> **Lifecycle methods don't gate on ownership.** Once `read` clears a
232> resource for write, `reconcile` can assume it's safe to proceed.
233> Don't repeat the ownership check inside `reconcile` — that just
234> hides the policy decision from the engine.
235
236### `precreate`
237
238Optional. Reserves a physical name (or stub resource) before
239`create` runs. This is what enables [circular
240bindings](/infrastructure-as-effects/circular-bindings) — Worker A can know Worker B's
241URL before either is fully created.
242
243### `list`
244
245Enumerates every existing resource of this type in the ambient
246scope (account, region, or zone), returning the same `Attributes`
247shape `read` produces — so each item is directly usable with
248`delete`. This powers account-wide operations like
249[`alchemy unsafe nuke`](/cli/nuke). It must paginate exhaustively;
250resources with no enumeration API (singletons, existence-only
251resources) return an empty array. Providers can opt out of nuke
252with `nuke.singleton` (delete only resets always-present config)
253or `nuke.skip` (the resource can never actually be deleted).
254
255### `tail` / `logs`
256
257Optional. Powers `alchemy logs --tail` (live log streaming) and
258`alchemy logs` (historical fetch). Each returns a `Stream` or
259`Effect` of `LogLine` values respectively.
260
261## Plan and apply
262
263Alchemy combines every provider's lifecycle operations into a single
264**plan**, then **applies** it.
265
266- **Plan** — for each declared resource, alchemy compares the
267 desired props against persisted state and uses `diff` to decide
268 whether it's a `create`, `update`, `replace`, or `noop`. Anything
269 in state but no longer declared is marked for `delete`.
270- **Apply** — alchemy walks the plan in dependency order and calls
271 the matching lifecycle operation on each provider, surfacing the
272 results as `Output`s for downstream resources.
273
274See [Resource Lifecycle](/infrastructure-as-code/resource-lifecycle) for how a
275resource moves through these phases over time.
276
277## Authoring a custom provider
278
279Providers are just Layers, so adding a new cloud or third-party API
280is "declare a [Resource type](/infrastructure-as-code/resource), implement a
281Layer" — and merge it alongside `Cloudflare.providers()` or
282`AWS.providers()`. The [custom provider guide](/infrastructure-as-code/custom-provider)
283builds a Stripe `Product` provider end-to-end.
284
285## Where next
286
287- [Custom Provider](/infrastructure-as-code/custom-provider) — step-by-step
288 walkthrough of a Stripe `Product` provider.
289- [Actions](/infrastructure-as-code/action) — deploy-time work that isn't a
290 resource.
291- [Resource lifecycle](/infrastructure-as-code/resource-lifecycle) — when each
292 operation fires.