| 1 | --- |
| 2 | title: Providers |
| 3 | description: Providers implement the lifecycle operations for a resource type — reconcile, delete, diff, read, and more. |
| 4 | --- |
| 5 | |
| 6 | A **Provider** implements the lifecycle operations for a resource |
| 7 | type. When you `yield*` a resource inside a Stack, alchemy looks up |
| 8 | the provider for that resource's type and calls the appropriate |
| 9 | lifecycle method. This is what gives meaning to a |
| 10 | [Resource](/infrastructure-as-code/resource) declaration — the engine plans, the |
| 11 | provider acts. |
| 12 | |
| 13 | ## Provider layers |
| 14 | |
| 15 | Providers are registered as Effect Layers. `Cloudflare.providers()` |
| 16 | and `AWS.providers()` return Layers bundling every built-in provider |
| 17 | for that cloud: |
| 18 | |
| 19 | ```typescript |
| 20 | Alchemy.Stack( |
| 21 | "MyApp", |
| 22 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 23 | Effect.gen(function* () { |
| 24 | yield* Cloudflare.R2.Bucket("Bucket"); |
| 25 | }), |
| 26 | ); |
| 27 | ``` |
| 28 | |
| 29 | The type system enforces this — using a Cloudflare resource without |
| 30 | `Cloudflare.providers()` raises a compile-time error. |
| 31 | |
| 32 | ## Multiple providers |
| 33 | |
| 34 | A stack can use any number of providers. Because providers are |
| 35 | Layers, combining them is just `Layer.mergeAll`: |
| 36 | |
| 37 | ```typescript |
| 38 | import * as Layer from "effect/Layer"; |
| 39 | |
| 40 | Alchemy.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 | |
| 52 | This 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 |
| 55 | the same way: |
| 56 | |
| 57 | ```typescript |
| 58 | providers: Layer.mergeAll( |
| 59 | Cloudflare.providers(), |
| 60 | AWS.providers(), |
| 61 | Neon.providers(), |
| 62 | StripeProviders, // your own custom provider layer |
| 63 | ), |
| 64 | ``` |
| 65 | |
| 66 | The type system tracks the requirements per resource: each resource |
| 67 | you `yield*` demands its own provider from the merged layer, so a |
| 68 | missing provider is a compile-time error, never a deploy-time |
| 69 | surprise. |
| 70 | |
| 71 | ## Lifecycle operations |
| 72 | |
| 73 | A provider is an object implementing some subset of these operations. |
| 74 | `reconcile` and `delete` are required; the rest are optional hooks |
| 75 | alchemy uses for richer behavior. |
| 76 | |
| 77 | ### `reconcile` |
| 78 | |
| 79 | Required. Called for every "make it so" intent — first-time |
| 80 | provisioning, routine updates, and adoption takeovers. Returns the |
| 81 | **output attributes** that downstream resources can reference. |
| 82 | |
| 83 | A reconciler is a single observe → ensure → sync → return flow: |
| 84 | |
| 85 | ```typescript |
| 86 | reconcile: 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 | |
| 96 | The 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 | |
| 105 | The reconciler must work for all three combinations. **Do not |
| 106 | branch the body on `output === undefined`** — that just renames the |
| 107 | old `create`/`update` split. Trust observed cloud state, not `olds`. |
| 108 | |
| 109 | `reconcile` must be **idempotent**: alchemy may retry it after a |
| 110 | state persistence failure. Deterministic physical names plus the |
| 111 | observe step ensure a retry finds the existing resource and re-syncs |
| 112 | any drifted fields. |
| 113 | |
| 114 | For a real reconciler built up step by step — observe, ensure, |
| 115 | sync, return, against the Stripe API — see the |
| 116 | [custom provider guide](/infrastructure-as-code/custom-provider#implement-reconcile). |
| 117 | |
| 118 | ### `delete` |
| 119 | |
| 120 | Required. Called when a resource is removed from code, replaced, or |
| 121 | when running `alchemy destroy`. Like `reconcile`, must be **idempotent** |
| 122 | — treat "already deleted" as success. |
| 123 | |
| 124 | ```typescript |
| 125 | delete: ({ output }) => |
| 126 | Effect.promise(() => stripe.products.del(output.productId)); |
| 127 | ``` |
| 128 | |
| 129 | ### `diff` |
| 130 | |
| 131 | Optional. Called during planning to decide what kind of change is |
| 132 | needed when properties differ. Returns a tagged `Diff` describing |
| 133 | the action: |
| 134 | |
| 135 | ```typescript |
| 136 | diff: 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 | |
| 147 | The 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 | |
| 161 | A provider can also declare top-level `stables` for attributes that |
| 162 | are immutable across all updates (e.g. ARNs, resource IDs). |
| 163 | |
| 164 | For comparing nested objects, use the `deepEqual` and |
| 165 | `anyPropsAreDifferent` helpers from `alchemy/Diff`. If any input |
| 166 | might still be unresolved when `diff` runs (an unresolved `Output`), |
| 167 | guard with `isResolved` first and return `undefined` to let the |
| 168 | default path handle it. |
| 169 | |
| 170 | ### `read` |
| 171 | |
| 172 | Optional. The engine consults `read` whenever a resource has no prior |
| 173 | state — both for **state recovery** (state lost between create and |
| 174 | persist) and for **adoption** (a fresh state store deploying against |
| 175 | existing cloud infrastructure). It is the single source of truth for |
| 176 | "does this resource exist, and is it ours?". |
| 177 | |
| 178 | ```typescript |
| 179 | read: 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 |
| 205 | to unwrap, just a hidden symbol on the attributes object: |
| 206 | |
| 207 | ```typescript |
| 208 | import { Unowned } from "alchemy/AdoptPolicy"; |
| 209 | |
| 210 | read: 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 |
| 220 | requires a previously-persisted ID (e.g. `output.productId`) should |
| 221 | return `undefined` in that case — they have no way to find the |
| 222 | resource 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 |
| 226 | always return plain attributes. The engine treats them as owned and |
| 227 | silent adoption is the default, so `--adopt` is unnecessary. |
| 228 | Resources with tag-based or naming-based ownership (most cloud |
| 229 | resources) 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 | |
| 238 | Optional. Reserves a physical name (or stub resource) before |
| 239 | `create` runs. This is what enables [circular |
| 240 | bindings](/infrastructure-as-effects/circular-bindings) — Worker A can know Worker B's |
| 241 | URL before either is fully created. |
| 242 | |
| 243 | ### `list` |
| 244 | |
| 245 | Enumerates every existing resource of this type in the ambient |
| 246 | scope (account, region, or zone), returning the same `Attributes` |
| 247 | shape `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; |
| 250 | resources with no enumeration API (singletons, existence-only |
| 251 | resources) return an empty array. Providers can opt out of nuke |
| 252 | with `nuke.singleton` (delete only resets always-present config) |
| 253 | or `nuke.skip` (the resource can never actually be deleted). |
| 254 | |
| 255 | ### `tail` / `logs` |
| 256 | |
| 257 | Optional. 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 | |
| 263 | Alchemy 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 | |
| 274 | See [Resource Lifecycle](/infrastructure-as-code/resource-lifecycle) for how a |
| 275 | resource moves through these phases over time. |
| 276 | |
| 277 | ## Authoring a custom provider |
| 278 | |
| 279 | Providers are just Layers, so adding a new cloud or third-party API |
| 280 | is "declare a [Resource type](/infrastructure-as-code/resource), implement a |
| 281 | Layer" — and merge it alongside `Cloudflare.providers()` or |
| 282 | `AWS.providers()`. The [custom provider guide](/infrastructure-as-code/custom-provider) |
| 283 | builds 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. |