Control-plane

The problem it solves

The control plane capability is the implementation, pithy dashboard is how you grant and revoke access, and connections explained is what a registration actually is.

A hosted platform can give you a dashboard trivially, because it already holds your data.

Pithy holds none of it. So a management client — the hosted dashboard, or one you write — has to reach into your Worker to see anything, and that reach is the thing that has to be designed rather than assumed.

Present, and denying by default

control-plane is a verification strategy like bearer or session. A route declaring it accepts calls from a registered management client and nobody else.

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.

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.

Asymmetric, and that is the whole design

The 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.

Three alternatives were considered and rejected, and the reasoning is worth having.

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 — it is their server talking to your Worker — and turning every adopter’s Worker into an OAuth provider is a large surface for a property nobody needs here. The one grant that would fit is a shared secret with extra ceremony.

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.

The data path

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.

Response bodies on that path never touch their origin, and their private key never leaves their server.

sequenceDiagram
    participant B as A browser
    participant MS as The client's server
    participant W as Your Worker
    B->>MS: Ask for a token
    MS->>MS: Sign with a key that never leaves
    MS-->>B: 60 seconds. One scope.<br/>Bound to a digest of its own body.
    B->>W: The call itself
    W-->>B: The response
    Note over MS: The response body never<br/>passes through here

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.

Some calls do transit — anything a dashboard aggregates or acts on server-side. For those the commitment is a zero-logging policy on your data: response bodies fetched from your Worker are not logged, cached or persisted.

One keypair per connection

Per customer, per project, per environment. A leak exposes one connection rather than a fleet, and rotation and revocation are naturally scoped to the same thing.

The client generates the keypair. The private key is born in their infrastructure and never leaves it — the alternative, your CLI generating one and sending it to them, puts key material on the wire for no benefit.

Sibling Workers are not separately addressable, and that is deliberate: 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.

Rotation: append, prove, then expire

Never replace. The order is the entire safety property.

  1. Append, in a call signed with the current key. Trust flows forward from existing trust, exactly as a rotated refresh token does. Both keys are now valid.
  2. Prove it with a real call signed by the new key.
  3. 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. A client still using the old key, naming a successor it has never actually used, would otherwise retire the one key that works.

Reverse those and a bad rotation locks the client out permanently with no authenticated path back. Two live keys is a normal state, which is what the versioned key array is for.

Capabilities bring their own admin routes

A capability contributing a management surface declares its admin routes with the scope each 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. The one address a client cannot discover is the seam’s own mount point, which is why it is stored on the connection.

Scopes are exact

No prefix rule, no wildcards. Holding a scope confers every route that requires it and nothing adjacent.

A connect’s default grant is every declared read, derived from the route table — and 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.

Every call is audited, reads included

Under its own actor kind, so what the dashboard did is answerable separately from what your users did.

Reads matter here more than writes. 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.

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 — which is a module you can import rather than a specification you transcribe.

ESC