Blog

What one command writes, and what it refuses to guess

2026-06-12

An isometric illustration of a large square block with a raised plus on its top face
$ pithy add auth

That installs a package, writes a registration into your config, adds bindings to every environment stanza of your wrangler.jsonc, mints the dev secrets auth needs to boot, and runs your migrations.

It also refuses to finish, twice, in situations where finishing would be worse. Those two refusals are the more interesting half.

What it writes

The package, with a name read from the catalog rather than built by interpolation. @pithy-sh/controlplane is a package that has never existed — controlplane ships inside @pithy-sh/core — so composing the name from the capability would reach for something that is not there. Your own package manager runs the install, detected from your lockfile.

Then it reads the real manifest, from node_modules. Not the catalog. The catalog is a discovery list; what the installed package says it needs is the contract, and that is what gets wired.

Your Worker’s config gains an import and a registration call. Your wrangler.jsonc gains the required bindings in every environment stanza the file declares, plus any Durable Object class migrations.

Handler source stays in the package. Only the thin registration lands in your repository, which is what makes upgrading a version bump rather than a merge.

Everything that can be completed offline is written: d1_databases, kv_namespaces, r2_buckets, ai, durable_objects, ratelimits, workflows.

And add touches no Cloudflare account. It writes config and local D1 only. Anything needing a real resource — a Vectorize index, a bucket, a secrets store — comes back as a note telling you which provision command creates it.

Refusal one: prerequisites, when nobody can be asked

Capabilities declare the seams they read. auth needs secrets and email. email needs secrets. Those are not suggestions — createBackend refuses to assemble a capability without them, so a Worker missing one does not start.

They are a graph, composed deepest first: secrets, then email, then auth. The order comes from walking the declarations, not from whatever order a manifest happened to list them in.

So what happens when you add auth to a project with none of them?

  • At a terminal, one question for the whole cascade: auth requires secrets, email. Compose them too? A nested prerequisite is never asked about twice. One intent, one question.
  • With --with-prerequisites, it composes them. That is the deterministic answer and the one for a script.
  • Anywhere else--json, no TTY, an agent driving the CLI — it refuses. Exit 1, naming the exact commands in the order they must run. The config is untouched.

The reasoning is worth quoting because it is the whole philosophy of the CLI:

Composing something nobody asked for is not a thing to do behind an adopter’s back. Reporting Done. on a project that cannot boot is worse.

Both failure modes are available and both are bad. Guessing yes installs three packages you did not ask for. Guessing no leaves you with a Worker that will not start and a command that said it succeeded. So where there is a human, ask; where there is not, refuse and say precisely what to run.

Refusal two: an option with no default

Some config options have no default, and that is a deliberate statement rather than an oversight.

A capability that states a default has an answer the kit is willing to pick. One that states none has an answer only you can give.

payments.billingSubject is the first of them: whether an entitlement is held by a person or a company. That answer lands in a column and a UNIQUE index. Get it wrong and fixing it later is a data migration across your purchase history.

So nothing guesses. A prompt asks with nothing to accept by pressing enter, and a run with no human is refused unless it carries --set billingSubject=….

The details that show someone used it

A few things that only appear after real use.

It is idempotent. A second pithy add auth changes nothing. Adding the same capability to a second Worker reuses the installed package and writes only that Worker’s wiring.

Dev secrets land outside your checkout, in a per-project config directory — the master key add secrets mints, the session secret auth needs. Not in your repo, so they cannot be committed.

The audit event fires on failure too. With credentials and the audit capability composed, add records capability/added whether it succeeded or not. And there is a nice edge case in the docs: the emitter is inert the first time you run pithy add audit, because nothing can audit-log its own installation.

A project needs a name first. It leads every resource name add proposes and claims the database the closing migration writes to. A project without one is told before anything is installed, rather than left half-configured around a database nobody owns.

Why this shape

The value is not that it saves typing. It is that the wiring is the same every time, in every environment stanza, with the binding names the package actually expects.

The class of bug this removes is the one where staging has a binding production does not, discovered on a deploy. That is not hard to avoid — it is just easy to get wrong once, at 6pm, in the third environment.