1---
2title: Local Provider
3description: Build the local implementation of a resource — dual registration with ProviderLayer.dual and the LocalProvider runner for long-running processes.
4---
5
6During `alchemy dev`, some resources shouldn't touch the cloud at all: a
7Worker runs in a local `workerd`, a dev server is a child process on your
8machine. A **local provider** is the implementation that serves a resource
9type in dev, alongside the **live provider** used by `alchemy deploy`.
10
11This guide is about *building* one. For the user-facing dev model — and
12opting a resource out of local emulation with `remote()` — see
13[Local development](/environments/local-development). If you haven't built
14a provider before, start with
15[Custom Provider](/infrastructure-as-code/custom-provider) — a local
16provider is just a second implementation of the same lifecycle contract.
17
18## What the engine does for you
19
20The engine remembers which implementation reconciled each resource. Moving
21a resource between local and live is planned as a **replacement**, and
22every instance is deleted by the provider that created it — so a deploy
23right after a dev session tears the local instances down with *your* local
24provider. You implement the lifecycle; the engine owns the switching.
25
26## Register both: `ProviderLayer.dual`
27
28```typescript
29import * as ProviderLayer from "alchemy/Local/ProviderLayer";
30import * as Layer from "effect/Layer";
31
32export const WorkerProvider = () =>
33 ProviderLayer.dual(Worker, {
34 live: () => LiveWorkerProvider(),
35 local: () =>
36 LocalWorkerProvider().pipe(Layer.provide(localRuntimeServices())),
37 });
38```
39
40`dual` registers the run's default mode eagerly and builds the other
41**lazily** — e.g. the local provider is only constructed during a deploy
42if it has an instance to tear down.
43
44### Route deploy-time data-plane calls: `dataPlane`
45
46Binding clients can be invoked at deploy time — inside an `Action` body or
47a plan-time `execute` — and their API calls must target whatever data
48plane the bound resource actually lives on. Pass the local mode's
49environment override (endpoint, credentials, region — the same layer the
50local lifecycle runs under) as `dataPlane`:
51
52```typescript
53ProviderLayer.dual(Bucket, {
54 live: () => BucketProvider(),
55 local: () => provideProviderContext(BucketProvider(), flociServices()),
56 dataPlane: flociServices,
57});
58```
59
60With it registered, a client bound to a local-mode resource is routed to
61the emulator even when the ambient profile carries live credentials, while
62a client bound to an `Alchemy.remote()` resource keeps hitting the real
63cloud — per bound resource, within the same Action.
64
65### Compose local deps inside the thunk
66
67Mode-specific dependency layers (`workerd`, Docker) go inside the `local`
68thunk, never globally — that's what keeps a plain `alchemy deploy` from
69constructing local machinery it doesn't need.
70
71:::note[Share dependency layers by reference]
72Variant builds are memoized by layer identity. If several local providers
73share a dependency layer (Cloudflare's `localRuntimeServices()`), the
74factory must return a module-memoized reference so they all get one
75instance per stack build.
76:::
77
78## Long-running processes: `LocalProvider.make`
79
80A process-shaped local provider has a problem ordinary providers don't:
81the "physical resource" is a running process that **state can't see** — a
82resource can be marked `created` while its process died with the last dev
83session. `LocalProvider.make` owns that machinery; you write how to boot
84one instance.
85
86### Declare the provider with `start`
87
88```typescript
89import * as LocalProvider from "alchemy/Local/LocalProvider";
90import * as Effect from "effect/Effect";
91
92export const DevProviderLocal = () =>
93 LocalProvider.make(
94 Dev,
95 import.meta.resolve("./Local.ts", import.meta.url),
96 Effect.gen(function* () {
97 const { spawn } = yield* CommandExecutor;
98
99 return {
100 start: Effect.fn(function* ({ news }) {
101 const child = yield* spawn(news);
102 const url = yield* waitForUrl(child);
103 return { url };
104 }),
105 } satisfies LocalProvider.LocalProviderSpec<Dev>;
106 }),
107 );
108```
109
110`start` boots one instance in the ambient `Scope` and returns Attributes at
111*readiness* — the process keeps running after `start` returns, until the
112runner closes the scope on restart or delete.
113
114### Handle processes that die on their own
115
116```diff lang="typescript"
117 start: Effect.fn(function* ({ news, invalidate }) {
118 const child = yield* spawn(news);
119 const url = yield* waitForUrl(child);
120+ yield* child.exitCode.pipe(
121+ Effect.exit,
122+ Effect.flatMap(() => invalidate),
123+ Effect.forkScoped,
124+ );
125 return { url };
126 }),
127```
128
129`invalidate` drops the instance from the running registry, so the next plan
130reports `update` and reconcile boots a fresh one.
131
132### Narrow the restart surface with `resolveConfig`
133
134```diff lang="typescript"
135 return {
136+ // Only these fields decide whether a running instance must restart.
137+ resolveConfig: ({ news }) =>
138+ Effect.succeed({ command: news.command, env: news.env }),
139 start: Effect.fn(function* ({ config, invalidate }) {
140- const child = yield* spawn(news);
141+ const child = yield* spawn(config);
142 // ...
143 }),
144 };
145```
146
147The runner canonically hashes `resolveConfig`'s result to decide
148noop-vs-restart, and hands the same value to `start` — so "what changed?"
149and "what starts?" can never drift.
150
151:::tip[Config must be plain data]
152`resolveConfig` runs inside `diff` on every plan: keep it cheap,
153side-effect-free, and free of closures or live objects. Derive plain
154*descriptors* here and materialize runtime-rich structures from them inside
155`start`.
156:::
157
158### Clean up cross-restart state with `stop`
159
160```diff lang="typescript"
161 return {
162 resolveConfig: ({ news }) => ...,
163 start: Effect.fn(function* ({ config, invalidate }) { ... }),
164+ // Delete-only: NOT called on restarts.
165+ stop: Effect.fn(function* ({ id }) {
166+ yield* stopProxy(id);
167+ }),
168 };
169```
170
171Anything acquired by `start` dies with the instance scope; `stop` is only
172for state that intentionally *survives* restarts — like a URL proxy kept so
173the address stays stable across rebuilds.
174
175### What the runner generates
176
177- **`diff`** — running instance + equal config hash → `noop`; anything
178 else → `update`. A fresh dev session has an empty registry, so processes
179 restart even though they were previously `created`.
180- **`reconcile`** — reuses the running instance on a hash match; otherwise
181 tears it down and forks `start` in a scope that outlives the deploy.
182- **`delete`** — tears down only the matching `instanceId` (a replacement's
183 old-generation delete can't kill its successor), then runs `stop`.
184- **`list`** — every running instance's Attributes.
185
186:::note[The dev sidecar]
187The second argument (`serverEntryUrl`) serves the provider over RPC: during
188`alchemy dev` the registry of running processes lives in a sidecar that
189survives user-code hot reloads. Outside dev, the provider is constructed
190in-process and no sidecar is spawned.
191:::
192
193## Not everything is a process
194
195If the local "instance" is just an entry in a shared registry — Cloudflare's
196local Queue is an entry in `LocalRuntimeState`, reconciled with three
197idempotent map operations — skip `LocalProvider.make` and write a plain
198provider registered via `dual`.
199
200## Testing
201
202```typescript
203const { test } = Test.make({ providers, dev: true }); // dev-mode default
204
205test.provider("promotes a dev worker to a live worker", (stack) =>
206 Effect.gen(function* () {
207 yield* program.pipe(stack.deploy); // local (dev default)
208 // Same props, pinned live: planned as a REPLACEMENT — the live
209 // provider creates the deployed worker, the local provider kills
210 // workerd.
211 yield* program.pipe(Alchemy.remote(), stack.deploy);
212 yield* stack.destroy();
213 }),
214);
215```
216
217`Test.make({ dev: true })` gives the file `alchemy dev` semantics, and
218`remote()` works in tests exactly like in programs.
219
220## Reference implementations
221
222- [`Command/Dev.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Command/Dev.ts)
223 — the smallest `LocalProvider.make` provider: spawn a command, extract
224 its URL, restart on prop changes.
225- [`Cloudflare/Workers/LocalWorkerProvider.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Workers/LocalWorkerProvider.ts)
226 — the full-size one: bundler watch loop, `workerd` instances, a URL
227 proxy torn down in `stop`.
228- [`Cloudflare/Queues/Queue.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Queues/Queue.ts)
229 — the registry-style counter-example: a plain local provider under
230 `dual`.
231
232## Where next
233
234- [Local development](/environments/local-development) — the user-facing
235 dev loop, and opting resources out of local emulation with `remote()`.
236- [Custom Provider](/infrastructure-as-code/custom-provider) — the
237 lifecycle contract both implementations share.
238- [Resource lifecycle](/infrastructure-as-code/resource-lifecycle) —
239 when each lifecycle method fires, including replacements.
240- [Testing Providers](/testing/testing-providers) — the `test.provider`
241 harness used above.