pithy dashboard connect --env prodYou run this, against your own project. It is the moment you grant a management client the right to call your Worker’s administrative routes — and until you run it, every one of them answers controlplane/not_connected.
Before you run it
Compose the seam, on the Worker that serves your app’s data:
pithy add controlplaneThat opens nothing. It composes a capability that is present and denying.
A deployed environment needs credentials, because the connection row is written into that environment’s real D1 over the REST API. --env dev resolves from local state and needs no account at all, which makes it the right place to try this first.
What happens, in order
sequenceDiagram
autonumber
actor You
participant CLI as pithy CLI
participant Client as Management client
participant D1 as Your D1
participant Worker as Your Worker
CLI->>CLI: Resolve the address from your project
Note over CLI: Printed before anything is registered
CLI-->>You: Every operation your Worker exposes
You->>CLI: The grant you chose
CLI->>Client: Start the device-code flow
You->>Client: Your browser, your approval
Client->>Client: Generate the keypair, keep the private half
Client-->>CLI: The public half, and the granted scopes
CLI->>D1: Write the public key
Client->>Worker: Signed ping
Worker-->>Client: Round-trips
Note over CLI,Worker: Only now does it say Connected
- The address is resolved from your project. The Worker’s domain declaration for that environment, then its route, then a hand-set base URL var. It prints what it found and where it came from before registering anything.
- You choose the grant. At a terminal, a list of every operation your Worker exposes, described in each capability’s own words, preselected to the default. Connection scopes is what the default is and why.
- A device-code flow starts. A short user code, your browser, your approval. The same shape as
wrangler login, and this leg is genuine user delegation — which is why a browser flow belongs here rather than on the machine-to-machine leg. - The client generates the keypair and keeps the private half. The public half comes back.
- The CLI writes it into your D1. This is the one key the CLI ever writes.
- A signed ping round-trips against your Worker.
Nothing reports connected until step 6
That is the part worth understanding, because it is the difference between this and a registration form.
A row written into your database proves that a row was written. It does not prove that the client can reach your Worker, that the address resolved to the right place, or that the base path is where the seam actually mounts.
A registration that was written but cannot be reached is a dead link, and reporting it as connected is how somebody finds out weeks later — when a pane fails and nobody can say when it started.
So a registration whose ping does not round-trip is reported as needs_reconnect, and the command deliberately does not print Done.
What a successful run looks like
$ pithy dashboard connect --env prod
acme-api → https://api.example.com/control-plane (declared in pithy.config.ts)
Open <the client's verification uri> and enter WXYZ-1234.
▸ Waiting for approval...
Connected prod.
Connection b6a1f0c2-…
Issuer https://app.pithy.sh
Worker https://api.example.com
Scopes manifest:read, keys:rotate, auth:users:read, audit:events:read, email:jobs:read
Key cpk_2026_07
Done.The first line is the resolution, printed before anything is registered so you can stop if it names the wrong address.
The client’s answer is checked, not trusted
A scope the client returns that you did not request is a management client trying to widen its own grant, and the connection is refused outright rather than quietly stored narrower.
That check exists because the client is the one that generates the keypair, and a client that can influence its own grant during that exchange has more power than the design intends.
Re-pointing an existing connection
An address changes — a custom domain, a renamed Worker, a moved environment. The repair is a re-point rather than a rebuild:
pithy dashboard connect --env prod --update --worker-url https://api.example.com--update with nothing to update is refused, naming what it accepts. On an update, passing no scope means leave the grant alone rather than reset it.
If it fails
The origin could not be reached. The refusal names the origin and says which of it answered nothing and it answered wrongly happened — so the override flag is only suggested where re-checking it would help.
The sign-in request expired. The flow polls until the authorization’s own expiry and then stops rather than running forever. Run it again.
Several Workers, none named. The administrative surface belongs on one Worker per project, and connect refuses the ambiguity rather than guessing. A Worker composing no seam is refused outright.
needs_reconnect. The row is written and the ping did not come back. Check the address the first line printed, and whether that environment is actually deployed.