Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control-plane HTTP API

The control-plane API is the transport the CLI speaks to a server. Most operators never call it directly — the boatramp subcommands wrap it — but it is a stable, documented surface for building your own tooling. This page lists the endpoints; the CLI reference maps each command onto them.

Conventions

  • Base path. Every control-plane endpoint is under /api. Public serving (host-routed content, /_sites/*, /healthz) is a separate, unauthenticated surface.
  • Authentication. A bearer token in Authorization: Bearer <token>. Every /api/* request is authenticated and authorized, except the handful gated by their own single-use credential (bootstrap, join, OIDC exchange). The exact right each endpoint requires is in the request-to-right mapping.
  • Bodies. Requests and responses are JSON, except blob upload (raw bytes) and /api/metrics (Prometheus text).
  • Errors. A non-2xx status carries a JSON { "error": "..." }. 401 is a missing or invalid token; 403 is a valid token without the required right.

Projects

A project owns sites, functions, and compute, and is the tenant boundary. Since 0.2.0 every site/function/compute/workflow endpoint has a project-scoped counterpart under /api/projects/:project/…; the legacy top-level paths (/api/sites/…, /api/functions/…, /api/compute/…, /api/workflows/…) target the reserved default project and stay byte-identical to pre-0.2.0.

MethodPathPurpose
GET/api/projectsList projects.
POST/api/projectsCreate a project.
GET/api/projects/:projectGet one project’s record.
DELETE/api/projects/:projectDelete an empty project (refused while it owns resources or is default).
any/api/projects/:project/sites/…Per-project site endpoints — the same shapes as Sites & deployments, scoped to the project.
any/api/projects/:project/{functions,compute,workflows,graphql}/…Per-project function / compute / workflow / GraphQL-admin endpoints, scoped to the project.

Sites & deployments

The paths below target the default project; the /api/projects/:project/sites/… counterparts are identical but scoped to :project.

MethodPathPurpose
GET/api/sitesList sites.
POST/api/sites/:site/deploymentsCreate a deployment from a manifest.
GET/api/sites/:site/deploymentsList a site’s deployments.
GET/api/sites/:site/deployments/:idGet one deployment.
POST/api/sites/:site/deployments/:id/activateMake a deployment the live one.
GET/api/sites/:site/currentThe currently active deployment.
GET/PUT/api/sites/:site/configRead / replace the site config.
GET/PUT/DELETE/api/sites/:site/aliases/:nameManage named aliases.
GET/api/sites/:site/aliasesList aliases.

Blobs

MethodPathPurpose
PUT/api/blobs/:hashUpload a content-addressed blob (raw body; the server verifies the hash).

Blob-backend migration (node-level)

Complete a zero-downtime blob-backend switch on a managed node reachable only over the control plane (no fly ssh / local shell). These operate on the node’s own configured blob store — never a client-named backend — so they are gated at system · admin (the destructive drain/purge) or system · read (the status probe), like prune / scrub / sql-move. The singular hyphenated paths (blob-drain, blob-status, blob-purge) deliberately do not collide with the /api/blobs/… (Blobs · Deploy) content-upload matcher, so a ship-only publisher can never reach them.

POST /api/blob-drain

Since 0.6.3. Drain the node’s own configured [serve].blob_fallback read-fallback secondary → primary (Storage::drain_pair on the live composite), so the fallback can be removed. The client guides; the running daemon (which already holds both backends open) executes. system · admin.

The body names no source or destination — the daemon drains only its own configured pair:

{ "dry_run": false, "concurrency": 8, "prefix": "" }

All fields are optional (deny_unknown_fields); dry_run defaults to false, concurrency to the engine default (8), an absent/empty prefix drains every object.

The response is an NDJSON stream (application/x-ndjson) — one JSON object per line — kept alive over a chunked connection so an edge idle-timeout can’t cut a long copy (the copy is idempotent and resumable, so a dropped connection is safe to re-run). Progress lines are tagged "type":"progress"; the final line is tagged "type":"report":

{"type":"progress","done":2,"total":3,"copied":2,"skipped":0,"copied_bytes":4096,"dry_run":false}
{"type":"report","total_objects":3,"copied_objects":3,"skipped_objects":0,"copied_bytes":6144,"verified":true,"dry_run":false,"secondary_drained":true,"message":"SECONDARY FULLY DRAINED — safe to remove [serve].blob_fallback and restart the node."}

secondary_drained is true only on a verified-complete, non-dry-run drain. A verify failure reports secondary_drained:false plus the missing keys. With no [serve].blob_fallback configured, the request is a 422 ({ "error": "no blob_fallback configured; nothing to drain" }).

GET /api/blob-status

Since 0.6.4. Query the node’s structured transition-mode state — whether a read-fallback secondary is currently attached (the node is mid-migration) — instead of grepping the startup log warning. system · read.

{ "blob_fallback_active": true }

POST /api/blob-purge

Since 0.6.4. Reclaim a provably-safe-only object set on the node’s blob store. Dry-run by default (report only, delete nothing); apply: true deletes. system · admin.

{ "mode": "unreferenced", "apply": false, "prefix": "" }

deny_unknown_fields. mode is required and is one of:

  • "unreferenced" — on-demand garbage collection: delete content-addressed blobs no live deploy manifest references (nothing serving can break). Refused with 409 while a read-fallback secondary is attached (drain and drop [serve].blob_fallback first). prefix is ignored in this mode.
  • "drained_source" — the migration decommission: delete the read-fallback secondary’s objects that are byte-confirmed present in the primary (fail-closed — an unconfirmed object is kept). prefix restricts the scope. With no [serve].blob_fallback configured, the request is a 422.

The response is NDJSON (progress lines then a final "type":"report" line), the same shape as the drain:

{"type":"report","mode":"drained_source","apply":true,"considered":3,"purged":1,"would_purge":0,"skipped_unconfirmed":2,"purged_bytes":17,"message":"drained-source purge complete: reclaimed 1 confirmed-duplicated object(s) (17 byte(s)); kept 2 unconfirmed"}

See Migrate the blob backend.

Domains

MethodPathPurpose
GET/POST/DELETE/api/sites/:site/domains/:host/verificationManage a domain-ownership challenge.
POST/api/sites/:site/domains/:host/verification/checkCheck the challenge.
GET/api/sites/:site/domain-verificationsList pending verifications.

Tokens

MethodPathPurpose
POST/GET/api/tokensMint / list tokens.
DELETE/api/tokens/:idRevoke a token by its id.
POST/api/tokens/bootstrapMint the first admin token with the single-use bootstrap secret.
GET/api/auth/whoamiThe presented token’s own roles.
POST/api/auth/exchangeExchange an OIDC JWT for a short-TTL token (oidc feature).

Cluster

MethodPathPurpose
POST/api/cluster/join-tokenMint a single-use bearer mesh join token (admin).
POST/api/cluster/joinAdmit a joining node (gated by the join token in the body + a possession proof, not admin RBAC).
GET/api/cluster/membersList the Raft membership (node, voter, caught-up, leader, address).
POST/api/cluster/promotePromote a caught-up learner to a voter (leader-only).
POST/api/cluster/rotate-keyRotate this node’s mesh key (make-before-break).
POST/api/cluster/revokeRevoke a node from the mesh (durable tombstone + drop from quorum).

See Deploy a self-hosted cluster and Run on Kubernetes.

Root anchors

Make-before-break root-key rotation (auth rotate-root). Admin-scoped.

MethodPathPurpose
GET/api/auth/rootList the extra trusted root anchors.
PUT/api/auth/rootTrust a new root anchor ({ "pubkey": "alg:hex" }).
DELETE/api/auth/root/:pubkeyRetire a root anchor.

See Migrate the root key.

Certificates & cache

MethodPathPurpose
GET/api/certsTLS certificate status.
POST/api/cache/invalidateInvalidate cached responses.

Operations

MethodPathPurpose
GET/POST/api/pruneReport / delete unreferenced deployments.
POST/api/scrubDelete unreferenced blobs.
GET/api/metricsPrometheus exposition (always available).
GET/PUT/api/authz/policyRead / replace the RBAC policy.

Functions & workflows

Top-level (default-project) function and workflow endpoints; the /api/projects/:project/… counterparts scope to another project.

MethodPathPurpose
GET/api/functionsList functions.
GET/PUT/DELETE/api/functions/:nameManage one function (its current version).
POST/api/functions/:name/versionsDeploy a new function version.
POST/api/functions/:name/rollbackRoll back to a prior version.
PUT/DELETE/api/functions/:name/aliases/:labelManage a version alias.
POST/api/functions/:name/invokeInvoke synchronously / async / scheduled.
GET/api/functions/:name/invocations/:idGet an async invocation record.
GET/POST/DELETE/api/functions/:name/triggers[/:id]Manage event triggers (webhook/queue/cron/blob).
GET/api/functions/:name/usageMetering / quota counters.
GET/PUT/DELETE/api/workflows/:nameManage a declarative workflow.
GET/api/workflows/:name/runs[/:id]List / get workflow runs.

Compute

Top-level paths target the default project; /api/projects/:project/compute/… scopes to another project.

MethodPathPurpose
GET/api/computeList compute workloads.
GET/PUT/DELETE/api/compute/:nameManage one workload.
POST/api/compute/:name/execRun a command inside a running replica (docker-exec style; posture-gated by allow_compute_exec). Since 0.3.9.

The control-plane surface is uniform whether or not execution is available on the node. Only the microVM backend needs /dev/kvm; the native container backend instead needs the br-boatramp bridge and CAP_NET_ADMIN (both compute backends are Linux-only). On macOS / Windows compute runs through the remote-docker backend. See Run compute workloads.

Compute operations (node-global)

These operate across every tenant on the node and are gated at system · admin — not the per-project /api/compute/* right. Since 0.3.9 (volumes: 0.3.11; maintenance/diagnostics: 0.3.14).

MethodPathPurpose
GET/api/compute/volumesList persistent volumes (in-use vs orphaned).
DELETE/api/compute/volumes/:nameReclaim a volume (?force=true to remove one still referenced).
GET/api/compute/statusObserved per-replica runtime state (health, lifecycle phase, IP:port, backend).
GET/api/compute/ipamThe compute-bridge IP-pool allocation.
GET/api/compute/dnsThe internal-DNS fleet view.
POST/api/compute/dns/resolveResolve an internal name as a container would (diagnostic).
POST/api/compute/reconcileForce a reconcile pass.
POST/api/compute/maintenance/set-healthOverride a replica’s stored health.
POST/api/compute/maintenance/restartStop + relaunch a replica.
POST/api/compute/maintenance/netdiagRun a network diagnostic.

See Diagnose compute.

Managed SQL (operator)

Run a migration script or a single query against a managed co-located database via its sealed credential (resolved server-side). Project-owned (project · deploy); writes are additionally posture-gated. Top-level paths target the default project; /api/projects/:project/sql/… scopes to another project. Since 0.3.9 (ping: 0.3.14).

MethodPathPurpose
POST/api/sql/:db/execRun a migration / statement script against the managed database :db.
POST/api/sql/:db/queryRun a single query and return its rows.
POST/api/sql/:db/pingActive per-replica reachability probe (bypasses the stored-health gate).

Schema migrations (owner-gated)

Apply an ordered migration step set — function / sql / extension steps — to a managed database :db, run as the project’s non-superuser owner role and tracked in a host-owned ledger. Input is upload-then-trigger: the JSON step-set bundle is uploaded via PUT /api/blobs/:hash and referenced by hash. The mutating verbs are gated at project · admin (never the deploy-grade publisher right /api/sql/ uses); status needs only project · read. Top-level paths target the default project; /api/projects/:project/migrate/… scopes to another project. Since 0.4.25 (Postgres only).

MethodPathPurpose
POST/api/migrate/:db/applyApply the pending suffix of the bundle; body { bundle }. project · admin.
POST/api/migrate/:db/dry-runReport which ids would apply, running nothing; body { bundle }. project · admin.
POST/api/migrate/:db/baselineRecord the prefix through up_to as already-applied without running it; body { bundle, up_to }. project · admin.
GET/api/migrate/:db/statusRead the applied-migration ledger (id, ordinal, hash, kind, applied-at, origin). project · read.

See Run owner-gated schema migrations.

Provisioning drift-repair (owner-gated)

Converge a managed database’s provisioning against spec — create/seal the non-superuser owner role, re-own the database + its objects + the migration ledger, re-grant runtime privileges — idempotently and data-preservingly (boatramp project repair). Deliberately not under /api/sql/ (whose catch-all resolves to project · deploy, which a ship-only publisher holds — nesting there would be an escalation); its own /api/repair/* prefix gates both verbs at project · admin. Dry-run is the report-only posture the CLI uses by default. Top-level paths target the default project; /api/projects/:project/repair/… scopes to another project. Since 0.5.2 (all SQL backends).

MethodPathPurpose
POST/api/repair/:db/applyApply the reconcile — converge provisioning against spec. project · admin.
POST/api/repair/:db/dry-runDrift-detect + report the plan, changing nothing (the default CLI posture). project · admin.

Managed databases (declarative)

The project-scoped declarative front door onto the daemon-level managed-database provisioning stack (a per-tenant Postgres/MySQL whose credential boatramp mints and seals). These back boatramp db ls | get | status and the apply-manifest databases: block; there is no db create — the manifest is the sole authoring surface. Declaring a database mints owner-role identities (CREATE ROLE / owner-role DDL as superuser), so the write verbs gate at project · admin — the same owner-grade placement as /api/migrate/ and /api/repair/, above the project-deploy catch-all, so a project_publisher / deployer can never reach them; the reads are project · read. Top-level paths target the default project; /api/projects/:project/databases/… scopes to another project. Since 0.6.0.

MethodPathPurpose
GET/api/databasesList the project’s declared databases (each an ApplyDatabase). project · read.
GET/api/databases/:nameGet one declared database, or 404. project · read.
PUT/api/databases/:nameDeclare (persist + eagerly provision) a database from an ApplyDatabase body. 204 on success. project · admin.
POST/api/databases/:name/ensureRe-provision an already-declared database idempotently. 204. project · admin.
GET/api/databases/:name/statusThe stored declaration + a coarse provisioned signal. project · read.

The PUT body is a typed ApplyDatabase (deny_unknown_fields) — a safe projection that carries only the fields a project author may declare. Naming an excluded field (image, password_env / url_env / read_url_env / migration_url_env, path, compute) fails to parse — those are the security contract (arbitrary-image RCE, BYO-secret/SSRF, host-fs traversal, author-named workload). The credential is never carried or referenced; boatramp fully manages and seals it.

{
  "name": "app",
  "kind": "postgres",
  "version": 16,
  "size": "medium",
  "tenant": "shared",
  "tenant_scope": "project",
  "extensions": ["pgcrypto"],
  "rls_session": true,
  "tenant_guc": "app.tenant_id"
}

Only name and kind (postgres / mysql) are required. size is a small / medium / large preset (default small, → bounded vcpus/mem/volume, not raw VM knobs); tenant is single (default) / shared; tenant_scope is project (default) / site. The remaining optional fields — version, extensions, read_only, the RLS knobs (rls_session, tenant_guc, session_guc, tenant_all_marker), and the capped pool_max / connect_timeout_secs / startup_grace_secs — are omitted from a response when unset.

GET /api/databases returns a JSON array of ApplyDatabase; GET /api/databases/:name returns one. GET /api/databases/:name/status wraps the declaration with a workload handle and a coarse provisioned fact (never a secret):

{
  "database": { "name": "app", "kind": "postgres", "size": "medium", "tenant": "shared", "tenant_scope": "project" },
  "workload": "bramp-db-<project>-app",
  "declared": true
}

The declarative databases: manifest block that drives these routes is covered under Declare a managed database; see also Apply a manifest.

Secrets & email profiles

Project-scoped credential stores, gated by the secrets right (see RBAC). A value never leaves over the API — the list/show responses are metadata-only (secrets) or redacted (email). Present only when a [secrets] key envelope is configured (else a clear 501). Secrets since 0.3.10; email profiles since 0.3.18.

MethodPathPurpose
POST/api/projects/:project/secretsSet (seal) a secret { name, value }; returns metadata, never the value.
GET/api/projects/:project/secretsList secret names + metadata (no values).
DELETE/api/projects/:project/secrets/:nameDelete a secret.
PUT/api/projects/:project/email/profiles/:nameSet / partial-update an SMTP profile (sealed password); returns the redacted profile.
GET/api/projects/:project/email/profiles[/:name]List / show email profiles (redacted).
DELETE/api/projects/:project/email/profiles/:nameDelete an email profile.

See Store secrets and Send email.

Tenancy schema

The project’s per-table tenant-key map — the isolation boundary that scopes every guest query. Read with project · read; replace/clear with project · admin (above the publisher’s deploy right, so a publisher cannot redraw the boundary). Since 0.4.3.

MethodPathPurpose
GET/api/projects/:project/tenancyRead the tenancy schema.
PUT/api/projects/:project/tenancyReplace the tenancy schema.
DELETE/api/projects/:project/tenancyClear it (back to deny-by-default).

See Isolate tenants.

GraphQL

The subgraph registry, the operation safelist, and the composed supergraph — a project-owned surface. Top-level paths target the default project; /api/projects/:project/graphql/… scopes to another project. See Serve a GraphQL API.

MethodPathPurpose
PUT/DELETE/api/graphql/subgraphs/:nameRegister (SDL body) / unregister a subgraph; a publish recomposes and is rejected if it doesn’t compose.
PUT/api/graphql/subgraphs/:name/sqlRegister a SQL-backed subgraph by introspecting a site’s managed database.
PUT/api/graphql/subgraphs/:name/functionRegister a function-backed subgraph by introspecting its _service { sdl }.
GET/api/graphql/supergraphThe composed supergraph (subgraphs, @key entities, root fields).
POST/GET/api/graphql/safelistRegister a trusted operation (returns its hash) / list the safelist.
DELETE/api/graphql/safelist/:hashRemove an operation from the safelist.

A function that self-declares a subgraph auto-registers on deploy; pass ?register_subgraph=false to PUT /api/functions/:name to opt a deploy out. See Federation.

Observability

Present with the handlers feature.

MethodPathPurpose
GET/api/sites/:site/_boatramp/handlersPer-handler operator stats.
GET/api/sites/:site/_boatramp/logsCaptured per-site guest logs.
GET/api/sites/:site/_boatramp/logs/streamStream per-site logs (SSE).
POST/api/sites/:site/_boatramp/dlqDead-letter-queue operations.
GET/POST/api/projects/:project/_boatramp/bus/dlqShared project-bus DLQ: inspect (Project·Read) / purge·redrive·discard (Project·Admin). Since 0.4.24.
GET/api/projects/:project/_boatramp/bus/queue/{peek,replay,groups}Project-bus live-queue inspection (Project·Read). Since 0.4.24.
POST/api/projects/:project/_boatramp/bus/queue/{group,pause}Project-bus group reset·delete / pause·resume (Project·Admin). Since 0.4.24.
GET/api/functions/:name/_boatramp/logsCaptured per-function guest logs (project-owned read). Since 0.3.17.
GET/api/functions/:name/_boatramp/logs/streamStream per-function logs (SSE). Since 0.3.17.

The function-logs endpoints have a /api/projects/:project/functions/… counterpart scoped to another project. See Observe a running server.

Agent (MCP)

MethodPathPurpose
POST/GET/DELETE/mcpModel Context Protocol endpoint (streamable-http), for driving this node from an AI agent.

Unlike /api/*, /mcp is gated only by a valid plain bearer (not a specific right): each MCP tool call is separately re-authorized in-process against the forwarded token’s scope. On by default; toggle with mcp.enabled (daemon config). cnf/DPoP tokens are rejected — use a plain bearer or the stdio transport.

Public (unauthenticated) endpoints

Never token-authenticated. Visitor access control (basic auth / IP rules / rate limit) is applied per-site inside the serving handlers.

MethodPathPurpose
GET/healthzLiveness.
GET/readyzReadiness.
any/ (host-routed)Serve site content, selected by Host — see How a request reaches your site.
any/_sites/<name>/*Serve a site by name (admin/testing).
GET/_deploy/*Serve a deployment by id (an unguessable content-hash capability).