pithy add controlplaneNote the spelling: one word, no hyphen. The strategy literal on a route is control-plane, but every namespace token — the capability name, the migration namespace, the table prefix, the error domain — is controlplane, because those patterns forbid a hyphen.
It is already installed
This is the one capability whose package you already have. The seam ships inside @pithy-sh/core, which is every capability’s dependency and the runtime your Worker is built on — so @pithy-sh/controlplane is a package that has never existed, and the catalog knows that.
add installs nothing. It composes what is there.
The same fact shapes removal: pithy remove controlplane unwires the seam and uninstalls nothing, because uninstalling core would take the project with it.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration:
controlplane({
basePath: "/control-plane",
issuer: "https://app.pithy.sh",
}),apps/<worker>/wrangler.jsonc gains the DB binding, in every environment stanza — the registration rows live in your own app database.
One migration runs, creating the seam’s own tables.
issuer is the one thing that does not derive
Every other origin in the kit is derived from the Worker’s own address, because writing one down is how a staging deploy mails production’s users.
issuer is different: it is an identity, not an address. A connection stores the issuer it was created with, and verification checks that stored value — so a per-environment issuer would make a connection minted in staging unverifiable in production.
That may well be the better isolation, but it is a decision about trust rather than about reachability. Write it; do not derive it.
Compose it on one Worker
The administrative surface belongs on one Worker per project, and a connection targets that one.
Sibling Workers are not separately addressable, deliberately: the data being administered is shared through binding names rather than owned per Worker, so a second connection to a sibling would be a second credential onto the same rows.
In a project with several Workers, pithy dashboard connect refuses the ambiguity and asks which one rather than guessing — and a Worker that composes no controlplane() is refused outright, because there is nothing there to connect to.
Nothing is open yet, and that is the point
Adding the capability opens no access. Every control-plane route answers controlplane/not_connected until a client is registered, and there is no flag that changes that.
Connecting is a separate, deliberate command:
pithy dashboard connect --env prodThat runs a device-code flow in your browser, the management client generates the keypair and keeps the private half, and the CLI writes the public half into your D1. Nothing reports connected until a signed ping round-trips — a registration that was written but cannot be reached is a dead link, and reporting it as connected is how you find out weeks later.
Choosing the grant
At a terminal, connect lists every operation your Worker exposes, described in each capability’s own words, preselected to the default — which is every declared read, plus the seam’s own two. Narrowing is the point of showing you the list; --scope answers the same question headlessly.
A scope joins that default only when every declared route requiring it is a GET. One mutating route anywhere makes the whole scope a write, however it is spelled.
Without a dashboard
pithy dashboard connect --env prod --public-key ./client.jwk.json --issuer https://admin.example.comThat registers a key you generated, with no dashboard involved. Nothing proves it — the CLI holds no private half to sign with — so the status is registered rather than connected, and the command says so.
The contract your client is held to is a module you can import, carrying the calls, the response shapes and the origin. Implement it and let the compiler tell you what you owe, rather than copying field sets into a test that can drift.
What composing it turns on elsewhere
Every capability that contributes admin routes starts serving them. Auth’s user and device panes, payments’ manual entitlement grant and revoke, email’s job log and suppression list, audit’s event reader, secrets’ registry reader and rotation trigger.
All of them were present and denying before. Composing the seam does not open them either — the connection’s scopes do.
Check it worked
pithy dashboard status --env devBefore a connect, that reports connected: false, which is the shipped and correct state. After one, --verify proves it with a signed ping rather than guessing.