Skip to content

Layers

A Binding is the smallest unit of “use this resource”. A Layer is the next step up: take a group of resources and bindings, hide them behind a typed interface, and hand callers an object that just does the thing.

This is how alchemy lets a single Worker swap from D1 to Postgres, or from in-memory mocks to live cloud, by changing one line of configuration.

A real authentication module needs at least:

  • A database
  • A connection
  • A secret
  • Adapter glue between an HTTP handler and the auth library

Inlining all of that in every Worker that needs auth is fine for one project. By the fifth Worker, you’ve copy-pasted four versions of the same setup with subtle differences. By the time you want to move from D1 to Postgres, every Worker has to change.

Layers fix this by defining the contract once and the implementation separately.

A service is a Context.Service — a typed Tag that says “somewhere in the program, an implementation of this exists”:

packages/better-auth/src/BetterAuth.ts
import type { HttpEffect } from "alchemy/Http";
import { type Auth } from "better-auth";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
export class BetterAuth extends Context.Service<
BetterAuth,
{
auth: Effect.Effect<Auth<any>>;
fetch: HttpEffect;
}
>()("BetterAuth") {}

Anyone who depends on BetterAuth writes yield* BetterAuth and gets the typed object — no idea where it comes from. That’s the whole point.

A Layer creates whatever resources it needs and returns a value satisfying the service. Here’s the real @alchemy/better-auth implementation backed by Cloudflare D1:

packages/better-auth/src/CloudflareD1.ts
import { Random } from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import { betterAuth as makeBetterAuth } from "better-auth";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Redacted from "effect/Redacted";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { BetterAuth } from "./BetterAuth.ts";
export const CloudflareD1 = Layer.effect(
BetterAuth,
Effect.gen(function* () {
// 1. Declare the infrastructure this Layer needs.
const d1 = yield* Cloudflare.D1Database("BetterAuth");
const connection = yield* Cloudflare.D1Connection.bind(d1);
const secret = yield* Random("BETTER_AUTH_SECRET");
// 2. Build the better-auth instance, cached so it's created once.
const betterAuth = yield* Effect.gen(function* () {
return makeBetterAuth({
database: yield* connection.raw,
secret: yield* secret.text.pipe(Effect.map(Redacted.value)),
});
}).pipe(Effect.cached);
// 3. Return the service implementation.
return {
auth: betterAuth,
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const auth = yield* betterAuth;
const response = yield* Effect.promise(() =>
auth.handler(request.source as Request),
);
return HttpServerResponse.fromWeb(response);
}),
};
}),
).pipe(Layer.provide(Cloudflare.D1ConnectionLive));

Three things happen in one expression:

  1. Infrastructure is declared — the D1 database and connection are real resources. They go through plan/create/update like any other.
  2. Bindings are wiredD1Connection.bind(d1) connects the database to whatever Worker eventually consumes this Layer.
  3. A typed implementation is returnedauth and fetch satisfy the BetterAuth interface.

Step 3 — Provide the Layer to a Platform

Section titled “Step 3 — Provide the Layer to a Platform”

A Worker that wants auth doesn’t reach for D1 or better-auth directly. It depends on BetterAuth and provides whichever implementation it likes:

src/Api.ts
import * as Cloudflare from "alchemy/Cloudflare";
import { BetterAuth, CloudflareD1 } from "@alchemy/better-auth";
import * as Effect from "effect/Effect";
export default Cloudflare.Worker(
"Api",
{ main: import.meta.path },
Effect.gen(function* () {
const auth = yield* BetterAuth;
return {
fetch: auth.fetch,
};
}).pipe(Effect.provide(CloudflareD1)),
);

The Worker handler doesn’t know D1 exists. It asks for BetterAuth and gets it. Effect.provide(CloudflareD1) wires in the D1 layer — and along with it, the database resource itself joins the Stack.

Because the Worker depends on BetterAuth, not CloudflareD1, moving from D1 to Postgres is a one-line change. Imagine a hypothetical PostgresLayer:

// before
.pipe(Effect.provide(CloudflareD1))
// after
.pipe(Effect.provide(BetterAuthPostgres))

The next deploy:

  • Tears down the D1 database (no longer declared)
  • Creates whatever BetterAuthPostgres declares (a Hyperdrive binding, a Neon database…)
  • Updates the Worker’s bindings accordingly
  • Leaves the handler code untouched

The same trick works in tests: provide an in-memory layer, run the Worker as a normal Effect, assert on the result.

Layers compose with the standard Effect combinators:

CombinatorUse it for
Layer.mergeAll(a, b)Provide multiple independent services
Layer.provideMergeA Layer that supplies and exposes a service
Layer.provideSatisfy a Layer’s dependencies privately

A typical app stack looks like this:

.pipe(Effect.provide(Layer.mergeAll(
CloudflareD1, // provides BetterAuth
JobStorageDynamoDB, // provides JobStorage
RateLimiterKV, // provides RateLimiter
)))

For the deploy-time mechanics that make these layers work — IAM, typed clients, env injection — see Binding. For Workers that reference each other across Layers, see Circular Bindings.

The trick is that resources are Effects. They participate in Effect’s dependency injection like any other value. So a Layer that creates a database, binds it, and returns a typed service is just a normal Effect Layer — no special mechanism, no extra runtime.

That means:

  • Distribute on npm — A Service is just a TypeScript module exporting an interface and a Layer. Publish @org/sessions, npm install it elsewhere, Effect.provide it.
  • Test against any backend — Provide an in-memory Layer in tests; provide the cloud Layer in CI. The handler under test never changes.
  • Migrate without rewrites — Moving from one cloud primitive to another is a Layer swap, not a Worker rewrite.