Three nouns, and they nest in one direction.
A project owns a name, a set of environments, and a Cloudflare account. A Worker is a deployable script inside it. An environment is a deployment target that every Worker has a stanza for.
apps/ is the registry
One project or two is the decision above this, pithy worker is what adds to it, and adding a second Worker is the walkthrough.
treeView-beta
my-backend/
pithy.config.ts ## project identity and policy
package.json ## workspaces: apps/*
apps/ :::highlight ## the registry — every Worker lives here
api/
pithy.config.ts ## what THIS Worker is made of
wrangler.jsonc ## bindings, per environment
pithy.worker.jsonc ## the dev manifest, and the UI block
src/
index.ts ## the mount file
admin/
There is no root Worker. Every Worker lives in apps/<name>/ with its own config, and every command discovers them by enumerating that directory — there is no hand-maintained list anywhere.
Add, remove or rename one with pithy worker and the dev set, the deploy set and the migrate fan-out all follow automatically.
Two config files, and the split is deliberate
The root config holds only what cannot be per Worker: the project name, the environment list, the Cloudflare account, and one seeding safety policy.
Each Worker’s config holds what that Worker is made of — its capabilities and its own app capability.
Capabilities are per Worker because everything they drive is per Worker: the composed route tree, the bindings written into that Worker’s wrangler config, and Durable Object class migrations, which register a class against a specific script.
So a KV-only Worker never sees a D1 it does not use.
A Worker’s name is three strings
The directory apps/<name>/. The deployed script name. And a var that stamps every audit event.
They have to agree, and pithy worker rename moves all three at once. pithy doctor checks them on every run, so a hand-rename that misses one fails CI rather than a deploy.
Two of those are distinct identities worth keeping straight: the directory is what you type and what --worker accepts; the deployed script name is what the Cloudflare dashboard shows. A Worker scaffolded as web in project acme is web and acme-web at once, and a rename can leave the two unrelated entirely.
Every command that reports a Worker reports both.
Sharing is expressed in the binding name
Two Workers that both declare DB are backed by one D1. A Worker that wants its own declares a different binding.
That is the entire mechanism. Resource names carry no Worker segment, so the binding is what decides what is shared — and locally, Miniflare state lives at the project root rather than per Worker, because per-Worker state would silently split a shared database in dev and reunite it in production.
Environments
dev, staging, prod — local, test users, paid users.
dev is the top-level wrangler stanza rather than an env.dev. It is local, it always exists, and it is never declared: a project’s environment list names only what it deploys to.
Each other environment is an env.<name> block carrying its own bindings and its own ids. A stanza replaces rather than merges — so the vars block in each environment repeats all of them, which is why the scaffold writes them out rather than relying on inheritance.
A custom environment is fine, held to seven characters. Naming conventions is why.
The gates a scaffold ships with
pithy init writes three checks, not two — because CI needs the third:
| Script | Runs |
|---|---|
typecheck | The whole solution |
test | Node tests and Workers tests |
lint | The formatter and the two lint plugins |
The root TypeScript config is a solution file, and it has to be. It declares no files of its own and lists references instead.
The programs cannot be merged into one: a Worker needs the Workers types, a browser client needs the DOM, and a program carrying both makes a byte array structurally incompatible with a buffer source — which breaks every crypto call in the kit’s own signing code. Keeping the two type worlds apart is not tidiness; it is the only arrangement that compiles.
Each referenced program writes its build state under the project’s output directory, named after its Worker — deliberately not the Worker’s own, which the front-end build owns and empties. Two programs sharing one build-state file overwrite each other’s, and the incremental build goes quietly wrong.
The test config comes split by runtime. Workers tests run inside the real runtime against real D1 and KV through Miniflare; everything else runs in Node. That split is the kit’s whole testing argument — a test that mocks D1 proves the mock works — and the Workers half is the fiddly one to wire, which is why it is scaffolded rather than described.
One gap, stated rather than hidden: pithy worker add writes the new Worker’s TypeScript config but does not add it to the solution file. Add the reference yourself, or that Worker’s source is typechecked by nothing.
The formatter covers the files Pithy rewrites
wrangler.jsonc and the worker manifest are edited in place by several commands, through one comment-preserving printer that emits what the formatter would.
So a command’s output needs no formatting step of your own and passes the pre-commit hook the CLI itself installs. They were exempted from the scaffolded formatter once, which was the same defect in another form: it left the two files Pithy touches most as the two files nothing formats.