Skip to content

Layers

As your application grows, you’ll need to start sharing components of your code using Layers. A Layer lets you define a service contract and swap different implementations of that service in and out across your code — a different storage backend or a different cloud.

In Alchemy, Resources and Bindings can be encapsulated inside a Layer, meaning you can create shareable services that carry their own infrastructure and permissions. You can share these within your codebase, across packages, or publish them to npm for others to consume.

Let’s look at how they work.

A service is a Context.Service — a typed tag that names a capability:

import * as Alchemy from "alchemy";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
export class JobService extends Context.Service<
JobService,
{
getJob(id: string): Effect.Effect<Job, JobError, Alchemy.RuntimeContext>;
}
>()("JobService") {}

Consumers write yield* JobService and get this typed object. The signature mentions Alchemy.RuntimeContext (covered below) and nothing cloud-specific.

Layer.effect(JobService, …) builds the service — “to construct JobService, run this Effect”:

import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
export const JobServiceKV = Layer.effect(
JobService,
Effect.gen(function* () {
return {
getJob: Effect.fn(function* (id: string) {
// TODO: read the job from storage
}),
};
}),
);

Declare the storage — a real KV namespace, owned by the Layer:

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");
return {
// ...
};
}),
);

Bind it — kv is a typed client for reading and writing that namespace at runtime:

Effect.gen(function* () {
const MyKV = yield* Cloudflare.KV.Namespace("MyKV");
const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return {
// ...
};
}),

Implement the contract against the client:

return {
getJob: Effect.fn(function* (id: string) {
// TODO: read the job from storage
return yield* kv.get<Job>(id, "json");
}),
};

A Worker yields the service and provides the Layer:

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)),
),
),
);

Providing the Layer is what brings the infrastructure in. On the next deploy, the MyKV namespace is created in the Stack and bound to this Worker, so kv.get works inside its handlers.

The inner Layer.provide satisfies JobServiceKV’s dependency on the KV runtime binding privately. The handler sees only JobService:

JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding))

A Layer is a value. Provide JobServiceKV to a second Worker and both share the same namespace, because KV.Namespace("MyKV") has a stable logical id:

src/Admin.ts
export default Cloudflare.Worker(
"Admin",
{ main: import.meta.url },
Effect.gen(function* () {
const jobs = yield* JobService;
// ...
}).pipe(
Effect.provide(
JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)),
),
),
);

The Stack ends up with one MyKV namespace, bound to both Api and Admin.

Changing the implementation is a one-line change:

JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)),
JobServiceDynamo.pipe(Layer.provide(AWS.DynamoDB.GetItemHttp)),

This swap crossed clouds: the Worker still runs on Cloudflare, but its jobs now come from a DynamoDB table on AWS, with the credentials and permissions wired up automatically.

Alchemy’s own Bindings follow the same tag/Layer split (a contract and a Layer).

A Layer that needs an environment the host can’t supply is a compile error:

export default Cloudflare.Worker(
"Api",
{ main: import.meta.url },
handler.pipe(Effect.provide(JobServiceDynamo)),
// ✗ Type error: the Layer requires AWS `Credentials` and `Region`,
// which a Cloudflare Worker can't provide
);

And Alchemy.RuntimeContext marks methods that only work inside a deployed handler (Phases):

Effect.gen(function* () {
const jobs = yield* JobService;
yield* jobs.getJob("job-1");
// ✗ Type error: `RuntimeContext` only exists inside handlers
return {
fetch: Effect.gen(function* () {
return yield* jobs.getJob("job-1"); // ✓
}),
};
});

A typical app provides one Layer 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. Use Layer.mergeAll for independent services and Layer.provide to satisfy one Layer’s dependencies privately from another.

  • Circular Bindings — the tag/Layer split applied to two services that reference each other.
  • Bindings — the deploy-time mechanics Layers are built on.