Runtime API Reference
Internal REST API for health checks, plugin/app management, admin/auth, and API key management. Used by the CLI/TUI, the CPanel, and CI automation.
For the general architecture, see the runtime concept. For the pool that executes operations, see Worker Pool.
Base URL and Discovery
Section titled “Base URL and Discovery”| Scenario | API Path |
|---|---|
| Default | /api |
With RUNTIME_API_PREFIX="/_" | /_/api |
curl https://buntime.home/.well-known/buntime# { "api": "/_/api", "version": "1.0.0", ... }Authentication
Section titled “Authentication”Three independent layers:
1. CSRF (browser)
Section titled “1. CSRF (browser)”Applied to state-mutating methods (POST, PUT, PATCH, DELETE) on /api/*.
Requires an Origin header matching the server host. Bypassed for
X-Buntime-Internal: true (worker → runtime).
2. Root Key (RUNTIME_ROOT_KEY)
Section titled “2. Root Key (RUNTIME_ROOT_KEY)”Bootstrap key. Used to create the first scoped admin/editor keys on a fresh deploy.
curl -H "X-API-Key: $ROOT_KEY" ...# orcurl -H "Authorization: Bearer $ROOT_KEY" ...The root key:
- Bypasses CSRF.
- Bypasses plugin
onRequesthooks. - Appears as synthetic principal
root(withrole=admin,isRoot=true). - Helm exposes it as
buntime.rootKeyin the Secret.
Do not expose the root key to the browser. It is for bootstrap only. Pre-2026-05-20 this was named
RUNTIME_MASTER_KEY/masterprincipal — the rename is breaking; update any external consumer.
3. API Keys (created via API)
Section titled “3. API Keys (created via API)”Keys generated via POST /api/keys. Stored as SHA-256 hashes in a Turso DB
file at ${RUNTIME_STATE_DIR}/api-keys.db (Helm: /data/state/api-keys.db
on a per-pod RWO PVC).
The store uses @tursodatabase/database (local mode) or @tursodatabase/sync
(embedded replica mode, when RUNTIME_AUTH_DB_MODE=sync), with MVCC journal
enabled. No external dependency for the default local mode.
Backend evolution:
- Pre-2026-05-20: JSON file (
api-keys.json). Migrated to DB on first boot and renamed to*.migrated. - 2026-05-20:
bun:sqlite. Files are binarily SQLite-compatible. - 2026-05-20 (later): Turso DB. Same
.dbfile opens; journal upgrades to MVCC on next write. Addsmode=syncfor embedded replicas in multi-pod.
| Role | Access |
|---|---|
admin | All permissions |
editor | Install/remove apps and plugins, plugin config, worker ops |
viewer | Read-only (apps, plugins, workers, keys) |
custom | Explicit permissions selected at creation time |
Endpoints — Overview
Section titled “Endpoints — Overview”| Group | Base path | Purpose |
|---|---|---|
| Admin | /api/admin | Session validation for CPanel admin |
| Health | /api/health | Health, readiness, liveness probes |
| Workers | /api/workers | List, upload, delete workers (a.k.a. apps) |
| Plugins | /api/plugins | List, upload, reload, delete plugins |
| Keys | /api/keys | List, create, revoke API keys |
| Docs | /api/openapi.json, /api/docs | Spec + Scalar UI |
Details per group below.
Admin Session
Section titled “Admin Session”Three endpoints govern operator authentication. They accept the credential via:
X-API-Key: <key>header (programmatic clients, CLI)Authorization: Bearer <key>header (SDKs)buntime_api_keycookie (issued byPOST /api/admin/session— used by the cpanel)
Lifetime of the cookie is set by RUNTIME_CPANEL_SESSION_TTL (default 24h, accepts strings like 30m, 7d).
GET /api/admin/session
Section titled “GET /api/admin/session”Probe the current session. Returns the principal if any of the three credential channels resolves; otherwise 401.
# CLI (header)curl -H "X-API-Key: $KEY" https://buntime.home/_/api/admin/session
# Browser (cookie travels automatically)fetch("/_/api/admin/session", { credentials: "same-origin" })Response:
{ "authenticated": true, "principal": { "id": 1, "name": "Admin Console", "keyPrefix": "btk_abcd1234", "role": "admin", "permissions": ["workers:read", "workers:install", "keys:read"] }}The root key returns the synthetic root principal (isRoot: true, role: admin).
The frontend uses permissions only to show/hide UI — real authorization
happens in the runtime.
POST /api/admin/session
Section titled “POST /api/admin/session”Exchange an API key for an HttpOnly session cookie. Used by the cpanel login form so that all subsequent same-origin requests (including plugin iframes that cannot inject headers) authenticate via the cookie.
POST /api/admin/sessionContent-Type: application/json
{"key": "btk_..."}Responses:
200 OK— body{ authenticated: true, principal: {...} }, headers includeSet-Cookie: [REDACTED] HttpOnly; SameSite=Strict; Path=/; Max-Age=86400(andSecureon HTTPS).400 Bad Request— body orkeyfield missing/malformed.401 Unauthorized— key does not match the root key and is not in the store.
The runtime accepts the RUNTIME_ROOT_KEY here exactly like the header path — operators who only have the root key configured can still log in to the cpanel without provisioning a regular API key first.
DELETE /api/admin/session
Section titled “DELETE /api/admin/session”Clear the session cookie. Idempotent: returns 204 No Content regardless of whether a cookie was present.
DELETE /api/admin/sessionResponse: 204 with Set-Cookie: [REDACTED] Max-Age=0; Path=/; SameSite=Strict.
Environment variables
Section titled “Environment variables”| Variable | Default | Meaning |
|---|---|---|
RUNTIME_ROOT_KEY | (unset) | Operator bootstrap key. Matches before the store, returns synthetic root principal. |
RUNTIME_CPANEL_SESSION_TTL | 24h | Cookie lifetime, parsed via parseDurationToMs (accepts 30m, 2h, 7d, etc.). |
Health
Section titled “Health”| Route | Probe | Response |
|---|---|---|
GET /api/health | General | { ok, status: "healthy", version } |
GET /api/health/ready | Kubernetes readiness | { ok, status: "ready", version } |
GET /api/health/live | Kubernetes liveness | { ok, status: "live", version } |
All return 200 when healthy.
curl https://buntime.home/_/api/health/readyWorkers
Section titled “Workers”“Workers” here means deployed serverless artifacts that the WorkerPool can execute. The runtime treats apps and workers as the same concept — these endpoints manage them on the filesystem (workerDirs). Pre-2026-05-19 the same surface was published under
/api/appsand gated byapps:*; both were retired in favor of the worker vocabulary.
GET /api/workers
Section titled “GET /api/workers”Lists workers in RUNTIME_WORKER_DIRS. The runtime uses the filesystem only to
discover candidate package roots; the public name and version come from
package metadata (manifest.yaml, manifest.yml, or package.json). Folders
without package metadata are ignored because they are outside the supported
package format.
[ { "name": "my-worker", "path": "/data/apps/my-worker", "removable": true, "source": "uploaded", "versions": ["1.0.0", "1.1.0"] }, { "name": "@buntime/cpanel", "path": "/data/.apps/cpanel", "removable": false, "source": "built-in", "versions": ["1.0.0"] }]source is built-in for anything that comes from the Buntime project/image
and uploaded for external roots such as /data/apps. Only uploaded workers
are removable.
POST /api/workers/upload
Section titled “POST /api/workers/upload”Upload via multipart/form-data. Accepts .tgz, .tar.gz, .zip.
Archive contract (shared with /api/plugins/upload below):
- Extensions accepted:
.tgz,.tar.gz,.zip. Anything else →INVALID_FILE_TYPE. - Internal layout: files either at the archive root, or wrapped in a single
top-level
package/folder (npm-pack convention). Tgz auto-strips viatar --strip-components=1; zip detects + manually unwraps a singlepackage/folder if present. - Metadata source (read at the effective root, after unwrap):
manifest.yaml(ormanifest.yml) is preferred. Read keys:name,version.package.jsonis fallback. Same keys.nameis required (from either source). Missing name → 400.versionis optional — defaults to"latest"when neither file declares it.
- Scoped names supported:
name: "@scope/foo"parses correctly. See “Install paths” below.
Install path (workers) — derived from name + version:
name | version | Installed at |
|---|---|---|
my-worker | 1.0.0 | <workerDir>/my-worker/1.0.0/ |
@scope/my-worker | 1.0.0 | <workerDir>/@scope/my-worker/1.0.0/ |
my-worker | (missing) | <workerDir>/my-worker/latest/ |
If the install path already exists, the existing folder is removed first, then the archive contents are moved into place. This is an upsert, not a merge.
<workerDir> is the first writable entry in RUNTIME_WORKER_DIRS (selected by
selectInstallDir(); image-provided dirs starting with . are skipped).
curl -X POST \ -H "X-API-Key: $KEY" \ -F "file=@my-worker-1.0.0.tgz" \ https://buntime.home/_/api/workers/uploadErrors: NO_WORKER_DIRS (400), NO_FILE_PROVIDED (400),
INVALID_FILE_TYPE (400), PATH_TRAVERSAL (400).
POST /api/workers/:scope/:name/:version/{enable,disable}
Section titled “POST /api/workers/:scope/:name/:version/{enable,disable}”Toggle a worker version at runtime (no restart). Disabling writes
enabled: false to that version’s manifest.yaml (creating the manifest if
the worker shipped only package.json) and clears the worker-config cache. A
disabled version is treated as not-installed — its base path 404s. Use _ as
scope for unscoped workers.
# Disable hello-app 1.0.0 (unscoped)curl -X POST -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/workers/_/hello-app/1.0.0/disable"
# Re-enable a scoped worker versioncurl -X POST -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/workers/@acme/api/0.1.0/enable"GET /api/workers returns disabledVersions: string[] per worker so the
cpanel can render the right toggle state. Requires workers:install.
DELETE /api/workers/:scope/:name[/:version]
Section titled “DELETE /api/workers/:scope/:name[/:version]”Without version: removes the entire worker (all versions). With version: removes only that version.
# Full scoped workercurl -X DELETE -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/workers/@buntime/my-worker"
# Specific versioncurl -X DELETE -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/workers/@buntime/my-worker/1.0.0"
# Non-scoped worker — use `_` as scopecurl -X DELETE -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/workers/_/my-worker"Built-in workers cannot be removed. The runtime returns 403 with
BUILT_IN_WORKER_REMOVE_FORBIDDEN or BUILT_IN_WORKER_VERSION_REMOVE_FORBIDDEN.
Plugins
Section titled “Plugins”GET /api/plugins
Section titled “GET /api/plugins”Lists plugins detected in RUNTIME_PLUGIN_DIRS. The runtime uses the filesystem
only to discover candidate package roots; the public name comes from package
metadata (manifest.yaml, manifest.yml, or package.json). Folders without
package metadata are ignored because they are outside the supported plugin
package format.
[ { "name": "@buntime/plugin-keyval", "path": "/data/.plugins/plugin-keyval", "removable": false, "source": "built-in" }, { "name": "@acme/plugin-custom", "path": "/data/plugins/@acme/plugin-custom", "removable": true, "source": "uploaded" }]source and removable follow the same rule as apps: anything from the
Buntime project/image is built-in; external upload roots are uploaded.
GET /api/plugins/loaded
Section titled “GET /api/plugins/loaded”Lists active plugins in the registry (runtime state).
[ { "name": "@buntime/plugin-keyval", "base": "/keyval", "dependencies": [], "optionalDependencies": [], "menus": [{ "title": "KeyVal", "icon": "lucide:database", "path": "/keyval" }] }]POST /api/plugins/upload
Section titled “POST /api/plugins/upload”Same archive contract as /api/workers/upload (see above). Difference: install
path omits the version segment because the plugin loader does not scan
version subdirectories.
name | Installed at |
|---|---|
my-plugin | <pluginDir>/my-plugin/ |
@scope/my-plugin | <pluginDir>/@scope/my-plugin/ |
version from the manifest is read for the response payload but does not
affect the layout. If <pluginDir>/<name>/ exists, it’s removed first.
curl -X POST \ -H "X-API-Key: $KEY" \ -F "file=@plugin-custom.tgz" \ https://buntime.home/_/api/plugins/uploadPOST /api/plugins/reload
Section titled “POST /api/plugins/reload”Re-scans pluginDirs, performs a full reload, and refreshes the live HTTP
server’s native route table (server.reload()), so a freshly uploaded
plugin’s routes — including server.routes — go live without a process
restart. Use after a manual upload or filesystem edit.
curl -X POST -H "X-API-Key: $KEY" \ https://buntime.home/_/api/plugins/reloadPOST /api/plugins/:name/enable and POST /api/plugins/:name/disable
Section titled “POST /api/plugins/:name/enable and POST /api/plugins/:name/disable”Toggle a single plugin’s enabled flag at runtime (no restart). The name is
URL-encoded; scoped names work. Flips manifest.enabled on disk (surgical
edit, comments preserved), rescans, and refreshes routes.
# Disable a scoped plugincurl -X POST -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/plugins/%40acme%2Fplugin-x/disable"
# Re-enable itcurl -X POST -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/plugins/%40acme%2Fplugin-x/enable"Response: { "success": true, "data": { "name": "@acme/plugin-x", "enabled": false } }.
Errors: PLUGIN_NOT_FOUND (404), PLUGIN_MANIFEST_NOT_FOUND (404). Requires
plugins:install.
DELETE /api/plugins/:name
Section titled “DELETE /api/plugins/:name”name must be URL-encoded.
# Remove @buntime/plugin-customcurl -X DELETE -H "X-API-Key: $KEY" \ "https://buntime.home/_/api/plugins/%40buntime%2Fplugin-custom"Built-in plugins cannot be removed. The runtime returns 403 with
BUILT_IN_PLUGIN_REMOVE_FORBIDDEN.
API Keys
Section titled “API Keys”GET /api/keys
Section titled “GET /api/keys”Lists non-revoked keys. The secret is never returned, only keyPrefix.
{ "keys": [ { "id": 1, "name": "Deploy CI", "keyPrefix": "btk_abcd1234", "role": "editor", "permissions": ["workers:install", "plugins:install"], "createdAt": 1777660000, "lastUsedAt": 1777660300 } ]}GET /api/keys/meta
Section titled “GET /api/keys/meta”Returns supported roles and permissions. Used by CLI/TUI/CPanel to populate forms.
POST /api/keys
Section titled “POST /api/keys”Creates a key. The full secret is returned only once — the client must save it immediately.
curl -X POST -H "X-API-Key: $ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Deploy CI","role":"editor","expiresIn":"1y"}' \ https://buntime.home/_/api/keysexpiresIn accepts never, 30d, 90d, 1y, or compact duration (7d,
2w, 6m).
Response:
{ "success": true, "data": { "id": 1, "name": "Deploy CI", "key": "btk_...", // the only time this appears "keyPrefix": "btk_abcd1234", "role": "editor" }}DELETE /api/keys/:id
Section titled “DELETE /api/keys/:id”Revokes a key. The key being used to make the request cannot self-revoke (protection).
Documentation
Section titled “Documentation”| Route | Content |
|---|---|
GET /api/openapi.json | OpenAPI 3.1 spec |
GET /api/docs | Interactive Scalar UI |
Headers
Section titled “Headers”Request
Section titled “Request”| Header | Description |
|---|---|
Authorization: Bearer <key> | Alternative to X-API-Key |
X-API-Key: <key> | Master key or generated key |
X-Buntime-Internal: true | Bypass CSRF (worker → runtime) |
X-Request-Id | Correlation (auto-generated if absent) |
Origin | Required for mutating methods (CSRF) |
Response
Section titled “Response”| Header | Description |
|---|---|
X-Request-Id | Correlation for tracing/logs |
Error Format
Section titled “Error Format”All error responses follow:
{ "error": { "code": "ERROR_CODE", "message": "Human-readable message" }}Error codes per endpoint are documented in the tables above.
Rate Limiting
Section titled “Rate Limiting”Not implemented in the runtime. When enabled, it is the responsibility of
@buntime/plugin-gateway. Configure it in the gateway manifest.
Composite Examples
Section titled “Composite Examples”# 1. Create admin key from root keycurl -X POST -H "X-API-Key: $ROOT_KEY" -H "Content-Type: application/json" \ -d '{"name":"Browser Admin","role":"admin","expiresIn":"30d"}' \ https://buntime.home/_/api/keys | jq -r '.data.key' > admin-key.txt
# 2. Discovery + health checkAPI=$(curl -s https://buntime.home/.well-known/buntime | jq -r '.api')curl -s https://buntime.home${API}/health/ready
# 3. Upload + reload a plugincurl -X POST -H "X-API-Key: $KEY" -F "file=@plugin-custom.tgz" \ https://buntime.home/_/api/plugins/upload \ && curl -X POST -H "X-API-Key: $KEY" \ https://buntime.home/_/api/plugins/reload
# 4. Validate admin session in CPanelcurl -H "X-API-Key: $BROWSER_KEY" \ https://buntime.home/_/api/admin/session | jq '.principal.permissions'CPanel — Notes
Section titled “CPanel — Notes”The CPanel is published at /cpanel/ (e.g. https://buntime.home/cpanel/overview
is the default landing). Runtime sections (overview, keys, apps,
plugins) are first-class routes under /cpanel/; there is no /cpanel/admin
subpath. Behavior:
- Login: form asks for
X-API-Key. Saved insessionStorageunderbuntime:cpanel-api-key. - Auth: uses
/api/admin/sessionexclusively. Does not depend on a separate authentication plugin. - The CPanel cannot be blocked by an authentication plugin — its
manifest.yamlmarkspublicRoutes: { GET: ["/**"] }, so the SPA bundle is always reachable; the SPA itself enforces the API-key gate client-side. - Frontend uses only the returned
permissionsto hide actions; real authorization stays in the runtime. - Discovery: the frontend reads
/.well-known/buntimeand automatically adapts to/apior/_/api.
Operations available in the admin:
| Category | Actions |
|---|---|
| Keys | List, create (admin/editor/viewer/custom), revoke (except the one in use) |
| Apps | List with built-in/uploaded source, upload (.zip/.tgz/.tar.gz), remove only uploaded app/version |
| Plugins | List (filesystem + loaded) with built-in/uploaded source, upload, reload, remove only uploaded plugins |
Related Documentation
Section titled “Related Documentation”- The runtime concept —
RUNTIME_API_PREFIX, CSRF, headers. - Worker Pool —
/api/workers/*endpoints (metrics/stats). - Plugin System —
POST /api/plugins/reloadand hot reload.