@substrat-run/control-plane-api
The HTTP surface over HostAdmin — the audited control-plane transport (control-plane.md §4.5). It is the seam between the platform's admin operations (provision a scope, bind a hostname, grant a role, deploy a vertical) and whatever runs them: the CLI, the Console, and the Dashboard all call this one surface.
It is a transport, not a source of truth. Every route lands on a HostAdmin method, every mutation is audited there, and the same API runs over any scope host — the pure-SQLite adapter in CI, Durable Objects in production.
pnpm add @substrat-run/control-plane-apicreateControlPlaneApi
A Hono app that exposes HostAdmin over HTTP, with authentication and the audit trail wired in. It takes a ScopeHost (whose .admin is the HostAdmin it projects) plus the auth, deploy, and observability seams, and returns a plain Hono app you serve on Node, Workers, or in tests:
import { createControlPlaneApi } from '@substrat-run/control-plane-api';
const app = createControlPlaneApi({ host /* ScopeHost */, /* auth, deploy, … */ });
export default app;Every request is authenticated to a PlatformActorId (staff SSO/session) or a builder principal (a scoped push token), and that actor is the same one HostAdmin stamps into every audit row — the transport never invents authority, it carries it.
The surface
Route groups map one-to-one onto the HostAdmin capability groups:
- Tenants —
/tenants(+/status,/reap,/identities,/entitlements): the tenant registry, lifecycle, identity links, and per-tenant SKU grants. - Scopes —
/scopes,/tenants/:t/scopes/:sand its lifecycle verbs (configure,version,snapshots,restore,rewind,bookmarks,migrations,reap), the read-only data window (tables,tables/:table,query,export,health, and the six event reads over it —history, one record's story;facets, the outbox narrowed, grouped and counted;causeandeffects, one event walked backwards to what started it or forwards to what it set off;invocation, everything one call recorded, which is the grouping neither walk can reach;dead-letters, every delivery in the scope that gave up, newest event first), and/fleet/migrations. The table, query, health, denial and event reads (tables,tables/:table,query,health,denials,denials/summary,history,facets,cause,effects,invocation,dead-letters) run one ladder: resolve the scope record, then ask the vertical's own/internal/*route when one is bound and the co-located host otherwise — and on the delegated branch the transport writes the K-24 access row itself, throughHostAdmin.recordDelegatedRead, so an auditor cannot tell which branch served a read from the row it left. Two scope reads sit outside that ladder on purpose:exportalways goes throughHostAdmin.exportScopefirst, because that is the call that writes its access-log entry (the vertical supplies only the bytes when one is bound), andmigrationsis schema metadata, read from whichever side holds it with no access row at all. Two routes are forwarded to the scope's own vertical host rather than answered from the directory:GET …/owner-seatreads whether the instance's owner seat has been claimed, andPOST …/owner-claimmints the short-lived claim link (201), addressed to the scope's boundapphostname.owner-claimanswers409when no hostname is bound or the seat is already claimed; either route answers501when the scope is bound to no vertical (with the diagnosis) or the bound vertical keeps no owner seat — both callbacks are optional invertical-host, and the vertical's own status is forwarded unchanged. - Verticals —
/verticals(+versions,versions/:id/admit|reject,channels,channels/:c/promote,deploy,instances,listing,publish-request,install-block): the registry, admission, promotion, and the deploy path. A staff or service caller may narrowGET /verticalsto one tenant's slice —?ownerTenant=<id>for what it owns,?visibleTo=<id>to add the published tier, which is what its install catalog shows; both only narrow, and both are ignored for a builder session, whose list is already fixed to what it owns. A vertical has one channel —prod(dev/staging retired);channels/:c/promoterefuses a non-prodchannel with a400.GET /verticals/:slug/egress(staff-only, like the other observability reads) lays each deployed version's observed outbound hosts beside the ones it declared (D-46), so an admit decision can see the difference;?hours=(1–72, default 24) and?limit=(1–1000, default 500) bound the read, and the report says out loud when it is sampled or truncated — an absent host is not proof a version never called it.501on a control plane with no observed-egress seam. - Hostnames —
/hostnames(+/status,/verify): the K-26 hostname map and its Cloudflare-for-SaaS issuance/verification. - Connections —
/tenants/:t/connections(+/verify,/credential,/activity,/connection-grants): the integrations hub. A credential is verified against the provider when it is connected rather than merely stored,/credentialsays which one is loaded without revealing it, and/activityreads what the connection actually did. - Copies and erasure —
/directory/backupsand/directory/restorefor the directory itself,/scopes/:s/backupsand/snapshotsfor a scope, and/scopes/:s/subjects/:id/shred, which redacts the spine payloads and destroys the sealing key in one audited, idempotent call — returning a receipt the DSAR response is written from. Deliberately staff-only: a builder forwards the request, the platform executes it. - Platform grants —
/verticals/:slug/email-senderand/verticals/:slug/tenant-provisioner: the capabilities a vertical must be granted rather than configure for itself. - Roles, admin-log, ops-failures, meters, model-usage (meter 3: the lines a vertical's model host raised as
model-usageintents, listed and folded per tenant × model with the platform's margin applied at read time), and observability (/observability/logs,/observability/metrics,/observability/metrics-series— staff-only, script-grain, and/observability/tenant-metrics,/observability/tenant-metrics-series,/observability/tenant-logs— the only observability routes a non-staff builder may reach, because they answer about one tenant: a builder's tenant is forced from the principal, and a staff caller must name one, since answering fleet-wide on a forgotten parameter is the leak these exist to close) — the permission surface, the append-only audit history, operational failures, billable readings, and fleet telemetry. - Sweep runs —
/sweep-runs: the durable record of what a sweep pass touched, one row per unit (a connection swept, skipped or failed, a schedule run, a freshness verdict). Staff read fleet-wide; a builder's tenant is forced from the principal, the/ops-failuresposture exactly. - Service refs —
/service-refs: what a Cloudflare service ref means — each owned vertical's serving script and per-version archive scripts, mapped to the signals stamp{vertical, version}plus the label beside it. A directory read, deliberately outside the observability501guard, so the release-health and issues views can join whether or not a telemetry backend is configured. - Issues —
/issuesandPUT /issues/status: operational failures grouped by fingerprint into failure classes, most recently seen first, and the staff verdict on one (resolve, ignore, reopen —regressedis ingest's word and is refused). Staff-only: an issue is a fleet-scoped aggregate with no tenant column, so the forced-filter posture cannot narrow it. No cursor, by design — the grouping is the compression. - Denials —
/tenants/:t/scopes/:s/denialsand…/denials/summary: the refusals a scope recorded (K-35). The third log beside the two above — the admin log holds staff mutations, the K-24 access log staff reads, and this one the operations that were refused. Scope-local rather than directory-side, because a denial rolls its own operation back and so must be written where that operation ran; the summary is bucketed per (actor, permission) so a prober's volume cannot hide a quiet actor, or per operation with?groupBy=operation— the answer echoesgroupByso a caller can tell which question a vertical pushed before that parameter existed actually answered. An operation bucket's key is nullable: refusals that unwound no operation invocation form oneoperation: nullbucket that still counts towardtotal. /push-tokens— mints the scoped builder tokens asubstrat pushauthenticates with.
Routes are the shape only; enumerate the OpenAPI document for the exact request/response schemas. The package also ships the typed clients that consume this surface — ControlPlaneClient (Console/CLI) and the narrowed, tenant-scoped VerticalClient (an app provisioning itself) — plus the deploy helpers (deployManifest, createWfpUploader) that validate a bundle against the sandbox contract and upload it to Workers-for-Platforms.
The audited-transport role
The one property that must not be retrofitted (K-20): a surface that can act on the directory without a durable record of who acted is worse than none. This package is that guarantee at the network edge — it authenticates the actor, forwards to HostAdmin, and lets the kernel write the append-only row. Staff auth (SSO, MFA) gates exposing the surface; the audit property is built into it. See The control plane.
Versioning
0.91.1, AGPL-3.0. Pre-release (0.x): the surface changes without notice until the platform GAs.