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* Cachegives you a typed client:getreturns anEffect<Option<string>>, failures are typed, nothing isany. - 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.layerbuilds 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 itEffect 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 bindfix: 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.
3. Your services, in two tiers
Section titled “3. Your services, in two tiers”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.
5. Plan, then ship, all or nothing
Section titled “5. Plan, then ship, all or nothing”infra planreads your account and says what a ship would do. It changes nothing.infra shipdoes 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 destroytears an environment down. Production’s data needs--allow-data-loss.
6. The account remembers what’s yours
Section titled “6. The account remembers what’s yours”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 prunedeletes it. - Two deploys never race. Each app and environment has a lock.
- There’s history.
infra stateshows each ship, failure and teardown.
7. Workers talk through contracts
Section titled “7. Workers talk through contracts”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.
8. Everything has a stand-in
Section titled “8. Everything has a stand-in”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.
9. Checks that explain themselves
Section titled “9. Checks that explain themselves”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.
That’s it
Section titled “That’s it”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.