A hosted backend platform gives you a dashboard because it already holds your data. Pithy does not hold your data, so a dashboard has to reach into your Worker to see any — and that reach is the thing that has to be designed rather than assumed.
control-plane is a verification strategy, like bearer or session. A route declaring it accepts calls from a registered management client and nobody else.
Present, and denying by default
A Worker that composes the capability and has never been connected answers every control-plane route with controlplane/not_connected.
There is no flag to leave off, no backdoor, and nothing enabled silently. Connecting a client is a deliberate act, and until you perform it the seam’s only behavior is refusal.
Your Worker is the authority. The management client is a client. It holds a private key; you hold the public one, you decide what it may do, and you can revoke it without asking anyone.
The credential is asymmetric, and that is the whole design
The management client signs each call with an Ed25519 private key. You register the matching public key.
Nothing secret of yours ever reaches them, and nothing they hold is worth stealing from you. Every other property follows from that one.
Three alternatives were considered and rejected:
Not a shared secret. Simpler, and strictly weaker: their breach becomes your breach. A per-customer shared secret means a compromise on their side yields keys replayable against every customer’s production Worker until each of them rotates. Under asymmetric, a full compromise leaks private keys that are useless the moment you revoke the corresponding public one — and revoking is a row you delete.
Not OAuth. OAuth exists so a third party can act on behalf of an end user without seeing their credentials, and it needs an authorization server with consent and token endpoints. There is no user delegation on this leg, and turning every adopter’s Worker into an OAuth provider is a large surface for a property nobody needs here.
Not Cloudflare Access or mTLS. Both work, and both move the authorization decision out of your Worker into edge configuration. That requires you to run Zero Trust, and it breaks the one sentence this design exists to keep true.
Every call is narrow
A browser cannot hold a long-lived signing key, so the management client’s server mints a short-lived, single-scope, body-bound token and hands it to the browser, which calls your Worker directly.
Sixty seconds. One scope. Bound to a digest of its own body. Checked for replay.
Your Worker does not distinguish a browser call from a machine call, deliberately: both are the same token, verified by the same code. There is no second token class to get wrong, and no route whose auth model depends on which client made the call.
Every call lands in your audit trail under its own actor kind, so what the dashboard did is answerable separately from what your users did — reads included. Reading the user table hands a management client every customer’s address; if only the writes were recorded, the trail would show one revoked session and say nothing about the customer list walked on the way there.
Rotation is append, prove, then expire
Never replace, and the order is the entire safety property.
- Append the new public key, in a call signed with the current one. Trust flows forward from existing trust. Both keys are now valid.
- Prove it with a real call signed by the new key.
- Expire the old one — and that call must itself be signed with the successor. Naming a live key is not proof that you can sign with it; only signing with it is.
Reverse those, or swap without proving, and a bad rotation locks the client out permanently with no authenticated path back. Two live keys is a normal state, not an exception.
Capabilities bring their own admin routes
A capability contributing a management surface declares its admin routes with the scope each one needs, and the seam serves a manifest of them. So adding a capability adds its management surface with nothing to wire, and a client discovers what it may call rather than hardcoding paths.
Scopes are exact, and there are many of them. Reading a user is a privacy operation; revoking their sessions is an availability one. Scope matching has no prefix rule, so holding one confers exactly the routes that require it and nothing adjacent.
What it deliberately does not do
It grants no writes by default. A connect’s default grant is every declared read, derived from the route table — a scope joins it only when every route requiring it is a GET. One mutating route anywhere makes the whole scope a write, however it is spelled.
It does not let anyone sign in as your users. No route mints a session, and no read projects a session token.
It does not require the hosted dashboard. The seam is MIT and gated by nothing. Register a key you generated yourself and write your own management client against the published contract.
It does not phone home. Registration is a row in your D1. Revocation is that row deleted — immediate, unilateral, and needing nothing from the client.
When you would reach for it
When you want the hosted dashboard, or when you want to build an internal admin tool that reads production without a second copy of production.
Not for your app’s own admin screens. Those are ordinary routes with an ordinary session and a role check of your own — this seam is for a client that is not a user of your app.
What it needs
Nothing, and it is already installed: it ships inside @pithy-sh/core. pithy add controlplane composes it; connecting a client is the deliberate second step.