Control plane reference

Config

KeyDefault
basePath/control-planeWhere the seam mounts. Sent at registration
replayBackendd1d1 or kv. See calling from a browser
allowedOrigins[]Extra browser origins allowed to call the control-plane surface. Additive to issuer, never replacing it
corsMaxAgeSeconds600How long a browser may cache a preflight. 0 to 7200

The effective origin list is issuer plus allowedOrigins, deduped. They are different axes — issuer is who signed the token, an origin is where the page is — and one management client can legitimately serve pages from more than one, a staging console beside a production one. Additive means adding your own can never silently drop the one that was already working.

The hosted dashboard needs no entry. issuer already defaults to its origin, so a stock Worker answers its preflights with nothing configured.

The list gates the browser; iss still gates the token. Neither relaxes the other, and an origin here grants nothing a credential would not have had.

Set corsMaxAgeSeconds to 0 while you are working an allow-list out. The cached thing is a compile-time constant, so ten minutes normally costs nothing and saves the dashboard a round trip on every call — but a browser that has cached a refusal keeps refusing for the rest of the window after you have fixed allowedOrigins, which reads exactly like a change that did not take. Browsers cap the value themselves, so a larger number is a request rather than a guarantee.

The seam’s own routes

Shaped like every other route in the kit, and reached only by a registered connection.

RouteRequiresPurpose
GET /control-plane/pingAny verified callerConnectivity and key proof
GET /control-plane/manifestmanifest:readWhat this Worker composes, and how to call it
GET /control-plane/keyskeys:rotateWhich keys are live, their ages and windows
POST /control-plane/keyskeys:rotateRegister a new key. Signed with the one it replaces
POST /control-plane/keys/:keyId/expirekeys:rotateExpire a superseded key. Signed with the successor

Error codes

Each is domain/reason, read the way every error code in the kit is.

Code
controlplane/not_connectedNo connection registered for this environment. The shipped, denying state
controlplane/invalid_credentialOne code for every failing verification step
controlplane/insufficient_scopeVerified, and not granted this operation
controlplane/key_not_found
controlplane/key_conflict

controlplane/key_expired is an audit action, not an error code. It records that a superseded key was retired — the grammar is the same and the two are not.

The manifest

{
  "environment": "prod",
  "connectionId": "…",
  "version": "…",
  "capabilities": [
    {
      "name": "payments",
      "version": "1.4.0",
      "healthKeys": [],
      "health": null,
      "configKeys": [
        { "key": "billingSubject", "choices": ["user", "organization"],
          "summary": "What kind of thing holds a purchase in this project." }
      ],
      "config": { "billingSubject": "organization" },
      "adminRoutes": [
        { "method": "POST", "path": "/billing/entitlements/grant",
          "scope": "payments:entitlements:grant",
          "summary": "Comp an entitlement, or repair a purchase that verified but never projected." }
      ]
    }
  ],
  "grantedScopes": ["manifest:read", "payments:entitlements:grant"]
}

Note the path. This adopter mounted payments at /billing. Each capability builds its declaration from its resolved config, so path is the fully mounted path and no client hardcodes a mount point.

Empty is a value. A capability with no management surface reports [] rather than being absent, and configKeys/config carry [] and {} rather than being omitted.

Every scope the kit defines

CapabilityScopes
Control planemanifest:read · keys:rotate
Paymentspayments:catalog:read · payments:purchases:read · payments:subscriptions:read · payments:entitlements:read · payments:entitlements:grant · payments:entitlements:revoke
Authauth:users:read · auth:devices:read · auth:sessions:revoke · auth:users:logout · auth:devices:revoke
Auditaudit:events:read · audit:events:read_detail
Emailemail:jobs:read · email:jobs:retry · email:suppressions:read · email:suppressions:write · email:suppressions:delete
Ledgerledger:accounts:read · ledger:transactions:read
Secretssecrets:status:read · secrets:rotate
SupportSee the support capability
TestersSee the testers capability

Matched exactly. No prefix rule, no wildcard — why each one is split the way it is is worth reading before you grant any of them.

Tables

Table
pithy_controlplane_connectionsOne row per connection: the registered keys, the issuer, the environment
pithy_controlplane_replaysSpent token ids, keyed on the id alone, pruned after a successful claim

What outlives a call

Two things: the token id in the replay set, and the audit event. No session, no user row, nothing on c.var.auth.

The audit actor kind

control-plane, hyphenated — it names the strategy, and an enum member may carry a hyphen.

Every other namespace token is controlplane, unhyphenated, because the namespace and action patterns forbid one.

ESC