Config
| Key | Default | |
|---|---|---|
basePath | /control-plane | Where the seam mounts. Sent at registration |
replayBackend | d1 | d1 or kv. See calling from a browser |
allowedOrigins | [] | Extra browser origins allowed to call the control-plane surface. Additive to issuer, never replacing it |
corsMaxAgeSeconds | 600 | How 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.
| Route | Requires | Purpose |
|---|---|---|
GET /control-plane/ping | Any verified caller | Connectivity and key proof |
GET /control-plane/manifest | manifest:read | What this Worker composes, and how to call it |
GET /control-plane/keys | keys:rotate | Which keys are live, their ages and windows |
POST /control-plane/keys | keys:rotate | Register a new key. Signed with the one it replaces |
POST /control-plane/keys/:keyId/expire | keys:rotate | Expire 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_connected | No connection registered for this environment. The shipped, denying state |
controlplane/invalid_credential | One code for every failing verification step |
controlplane/insufficient_scope | Verified, 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
| Capability | Scopes |
|---|---|
| Control plane | manifest:read · keys:rotate |
| Payments | payments:catalog:read · payments:purchases:read · payments:subscriptions:read · payments:entitlements:read · payments:entitlements:grant · payments:entitlements:revoke |
| Auth | auth:users:read · auth:devices:read · auth:sessions:revoke · auth:users:logout · auth:devices:revoke |
| Audit | audit:events:read · audit:events:read_detail |
email:jobs:read · email:jobs:retry · email:suppressions:read · email:suppressions:write · email:suppressions:delete | |
| Ledger | ledger:accounts:read · ledger:transactions:read |
| Secrets | secrets:status:read · secrets:rotate |
| Support | See the support capability |
| Testers | See 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_connections | One row per connection: the registered keys, the issuer, the environment |
pithy_controlplane_replays | Spent 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.