Skip to content

How it thinks

There are only a few ideas here. Once they click, every feature is the same pattern with a different Cloudflare product behind it.

1. A resource is a class that’s also its client

Section titled “1. A resource is a class that’s also its client”
class Cache extends Kv.Namespace<Cache>()('cache') {}
class Db extends D1.Database<Db>()('main') {}

Each class is three things at once:

  • An Effect service. In a handler, yield* Cache gives you a typed client: get returns an Effect<Option<string>>, failures are typed, nothing is any.
  • A declaration. It says “this app has a KV namespace called cache”. The deploy side reads these to know what to make.
  • A Layer. Cache.layer builds the client from the Worker’s binding, so production wiring is automatic.

The same goes for D1, R2, queues, Durable Objects, Workflows, Workers AI, Vectorize, rate limits, secrets and the rest. Learn one, and you’ve learned the shape of all of them.

2. A Worker binds what it uses, and the compiler checks

Section titled “2. A Worker binds what it uses, and the compiler checks”
const fetch = Effect.gen(function* () {
const cache = yield* Cache; // the handler needs Cache…
// …
});
Worker.make('api', { fetch }).bind(Cache); // …so the Worker must bind it

Effect records in a program’s type which services it needs. bind compares that with what you pass. Leave one out and it’s a type error, in your editor, naming what’s missing:

the Worker uses Db, which it doesn't bind
fix: bind it: .bind(…, Db)

It works however deep the need is: through your own services’ Layers, through helper functions, through an HttpApi’s routes. The opposite direction is checked too: infra check warns about a resource a Worker binds but never uses.

Most apps have services of their own (a repository over the database, a mailer, a clock). A Worker takes them as Layers:

Worker.make('api', {
fetch,
layer: Visits.layer, // once per isolate: built with the resources' clients
// requestLayer: …, // once per request, in the request's scope
}).bind(Cache, Db);

What your Layers need counts toward what the Worker must bind, so Visits.layer needing Db means the Worker binds Db.

4. An app is the unit you plan, ship and tear down

Section titled “4. An app is the unit you plan, ship and tear down”
export default App.make('shop', { workers: [api, users, orders] });

Names in your account come from the app: the cache of the app visits is visits-cache. An environment is a complete copy under another name (--env pr-12 makes visits-pr-12-cache), so a preview never touches production.

  • infra plan reads your account and says what a ship would do. It changes nothing.
  • infra ship does it: makes missing resources, applies migrations, uploads each Worker as a new version, moves traffic to it a step at a time (10% → 50% → 100%), and at each step calls it and checks the new version answered. If anything fails, or you press Ctrl-C, everything it did is undone.
  • infra destroy tears an environment down. Production’s data needs --allow-data-loss.

forge-effect keeps a small D1 database in your account, forge-effect-state, recording which resources each app made. That gives you some lovely guarantees:

  • Nothing is taken by accident. A resource with your name that the app didn’t make stops the ship (name-taken), naming whose it is.
  • Nothing is deleted by accident. A resource you stop declaring is unbound and kept, marked an orphan, until infra prune deletes it.
  • Two deploys never race. Each app and environment has a lock.
  • There’s history. infra state shows each ship, failure and teardown.

When one Worker calls another, the call goes over Cloudflare’s native RPC, and its shape is a contract: schemas in a file both sides import.

export class Users extends Worker.Contract<Users>()('users', {
get: { payload: { id: Schema.String }, success: User, error: UserNotFound },
}) {}

The callee implements it (the compiler checks every method), the caller binds it (and gets typed calls back, errors and all), and infra plan flags a contract change that would break callers still running the old version. Durable Objects use the same idea, with an address.

Every resource has an in-memory version for tests, Kv.memory(Cache), sqlite(Db) (real SQLite, migrations applied), Queue.memory(Jobs), and so on, and Worker.handle(worker, request) runs a handler with them. When you want the real runtime, infra dev runs your whole app in workerd, wired exactly as it deploys.

infra check finds what’s wrong before a deploy (types, declarations, bundle sizes, and with --account, your account) and says each problem with a stable code, where it is, and the fix. infra explain <code> says more. --json gives the same to an agent. plan and ship run the checks first.

Resources that are clients, Workers that bind them, apps that ship all or nothing, an account that remembers, contracts between Workers, stand-ins for tests, and checks that explain. Every guide from here is one of those ideas meeting another Cloudflare product.

Next: Storage: KV, D1 and R2.