Layers
A Binding connects one Resource to a Worker or Lambda (Functions & Servers). A Layer is the next abstraction up: a unit of encapsulated infrastructure. It owns whatever resources and bindings it needs, returns a typed implementation of a service interface, and hides everything behind that interface.
This is where portability lives: a Worker written against an
abstract JobService doesn’t know whether its underlying storage
is a KV namespace, a DynamoDB table, or an in-memory map — it
depends on the service, and a Layer wires up the rest.
The encapsulation problem
Section titled “The encapsulation problem”Say you want a Worker that serves jobs from a KV namespace. The
straightforward thing is to bind the KV in the Worker’s init
closure and call it from fetch:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { fetch: Effect.gen(function* () { const job = yield* kv.get<Job>("job-1", "json"); return HttpServerResponse.json(job); }), }; }).pipe(Effect.provide(Cloudflare.KV.ReadWriteNamespaceBinding)),);This works, but the handler is welded to KV. The fetch body
mentions kv.get, knows the value shape comes back as "json",
and propagates NamespaceError. Moving the data to DynamoDB or
swapping in an in-memory fake for tests means rewriting fetch —
not just the storage wiring.
A Layer is the answer.
A service is a contract
Section titled “A service is a contract”A service is a Context.Service — a typed Tag that names a
capability without saying how it’s provided:
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Alchemy from "alchemy";
export class JobService extends Context.Service< JobService, { getJob(id: string): Effect.Effect<Job, JobError, Alchemy.RuntimeContext>; }>()("JobService") {}A consumer writes yield* JobService and gets the typed object —
nothing else. The signature deliberately mentions
Alchemy.RuntimeContext (more on this below) but says nothing about
KV, DynamoDB, or any specific cloud primitive.
A Layer encapsulates the infrastructure
Section titled “A Layer encapsulates the infrastructure”A Layer is the implementation side of a service. It declares the resources it needs, wires up bindings, and returns the typed value:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export const JobServiceKV = Layer.effect( JobService, Effect.gen(function* () { const MyKV = yield* Cloudflare.KV.Namespace("MyKV"); const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { getJob: Effect.fn(function* (id: string) { return yield* kv.get<Job>(id, "json"); }), }; }),);Three things happen in one expression:
- The KV namespace is a real resource. It joins the Stack, goes through plan/create/update like anything else.
- The binding is wired —
KV.ReadWriteNamespace(MyKV)attaches the KV namespace to whichever Worker eventually consumes this Layer. - A typed
JobServiceis returned. Callers see onlygetJob.
Cloud-agnostic consumers
Section titled “Cloud-agnostic consumers”A Worker that wants jobs depends on JobService and provides a
Layer to satisfy it:
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const jobs = yield* JobService;
return { fetch: Effect.gen(function* () { return yield* jobs.getJob("job-1"); }), }; }).pipe( Effect.provide( JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)), ), ),);The Layer.provide satisfies JobServiceKV’s own dependency on the
KV runtime binding privately, so the handler sees only JobService.
Swapping the implementation is a one-line change:
JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)),JobServiceDynamo.pipe(Layer.provide(AWS.DynamoDB.GetItemHttp)),The next deploy tears down the KV namespace and creates whatever
JobServiceDynamo declares. The handler is untouched.
The same duality runs one level down: alchemy’s own Bindings follow
the same Tag/Layer split — ReadWriteNamespace satisfied by
ReadWriteNamespaceBinding or ReadWriteNamespaceHttp — the same
one-line diff, enforced by the same types
(a contract and a Layer).
Building with Layers
composes both levels in one Worker.
Where runtime requirements live
Section titled “Where runtime requirements live”The handler that called kv.get directly had a fetch body typed
roughly:
Effect.Effect<Response, NamespaceError, Alchemy.RuntimeContext>The KV-specific error and the implicit dependence on a Cloudflare binding both leak into the handler’s signature. The Layer-wrapped equivalent collapses to:
Effect.Effect<Response, JobError, Alchemy.RuntimeContext>The Cloudflare-specific surface is gone — absorbed by the Layer:
export const ReadWriteNamespaceBinding = Layer.effect( ReadWriteNamespace, Effect.gen(function* () { const env = yield* WorkerEnvironment; // ← required here, once // ...returns a client that closes over env }),);The Worker that consumes ReadWriteNamespaceBinding satisfies
WorkerEnvironment in one place; downstream callers see only
RuntimeContext. Try the swap on a Cloudflare Worker:
.pipe(Effect.provide(JobServiceKV)) // requires WorkerEnvironment ✓.pipe(Effect.provide(JobServiceDynamo)) // requires AWS Credentials | Region ✗The DynamoDB-backed Layer requires AWS Credentials and Region,
which a Cloudflare Worker’s constructor can’t satisfy — the program
won’t type-check. Functions and Layers fit together by type, not by
convention.
Runtime as a colored function
Section titled “Runtime as a colored function”As Phases covered, the init closure runs at plantime and at cold start; the runtime closure runs only inside the deployed handler. Bindings live in init; the actual cloud calls live in runtime.
Alchemy.RuntimeContext is the Effect service that exists only
inside the runtime closure. It is not provided at plantime, cold
start init, or anywhere else. So an Effect like:
kv.get(...): Effect.Effect<Job | null, NamespaceError, Alchemy.RuntimeContext>is one the type system guarantees can only run in the runtime
phase. Move that call up into the init closure and the type checker
rejects it — RuntimeContext is not satisfied there.
You can think of it as a “color” in the sense of colored functions:
runtime function getJob(id: string): JobTypeScript doesn’t have keyword-level coloring, so Alchemy encodes the color as an Effect requirement. The compiler enforces the init/runtime boundary for you.
What you compose with
Section titled “What you compose with”A typical app stack mixes several Layers, one per capability:
.pipe( Effect.provide(Layer.mergeAll( JobServiceKV, // provides JobService BetterAuthD1, // provides BetterAuth RateLimiterDurable, // provides RateLimiter )),)Each Layer brings its own resources into the Stack and its own typed service into scope. Consumers stay declarative and ignorant of the underlying primitives.
| Combinator | Use it for |
|---|---|
Layer.mergeAll(a, b) | Provide multiple independent services |
Layer.provideMerge | A Layer that supplies and exposes a service |
Layer.provide | Satisfy one Layer’s dependencies privately from another |
Why this matters
Section titled “Why this matters”Because Layers are normal Effect Layers, the patterns scale all the way out:
- Distribute on npm. A service is just a TypeScript module
exporting a
Context.Serviceand one or more Layers. Publish@org/jobs,npm installit,Effect.provideit. - Substitute for tests. Provide a
JobServiceMemoryLayer backed by aMap; the Worker under test never knows. - Migrate without rewrites. Moving from KV to DynamoDB is a Layer swap, not a Worker rewrite.
- Type-safe portability. Cloud-specific requirements are
confined to Layer construction; the consumer-facing surface
speaks only
RuntimeContext.
Infrastructure becomes a value you can encapsulate, name, and substitute — like any other module.
Where next
Section titled “Where next”- Building with Layers —
build a
JobServiceLayer end to end, then swap KV for R2. - Circular Bindings — the Tag/Layer split applied to two services that reference each other.
- Bindings — the deploy-time mechanics Layers are built on.