This catalog lists the interfaces registered by the applications in this directory. It covers HTTP, WebSocket, CONNECT, OTLP, health, static-asset, and outbound event interfaces; each entry names the owning service and what the interface does. Route tables are collapsed by default — expand a summary line to see its routes.
The inventory is implementation-grounded:
- Control-plane product routes come from the controllers under
api/src, match the generatedopenapi.yaml, and use the/apiprefix applied inmain.ts. - Hosted BoxLite-compatible routes come from the controllers in
boxlite-rest. They are excluded from the product OpenAPI document because their contracts live inopenapi/box.openapi.yamland, for cloud-only members and invitations,openapi/admin.openapi.yaml. - The registry proxy's runner credential lookup is an internal service route. It is excluded from the Cloud OpenAPI document and generated customer clients; the proxy owns its request and response handling.
- Runner, proxy, and collector routes come from their runtime registration in
server.go,proxy.go, andconfig.yaml. - Static asset trees come from the
ServeStaticModuleregistrations inapp.module.ts. - Outbound and asynchronous interfaces come from
notification.gateway.ts,webhook-events.constants.ts, andkafka-exception.filter.ts. - Local-stack listeners come from the service specs in
services.pyandnative.py; the container-based environment's come fromlocal-dex-env.mjs.
openapi/box.openapi.yaml also describes portable capabilities that the hosted
service does not currently register, including snapshots, clone, export,
import, and runtime-wide metrics. They are intentionally absent below;
GET /api/v1/config reports snapshots, clone, export, and import as disabled,
and private tunnels as enabled; clients check that before every private prepare.
| API | Service | Base path or host | Deployed port |
|---|---|---|---|
| Control-plane API | apps/api |
/api |
3000 |
| Hosted BoxLite-compatible API | apps/api |
/api/v1 |
3000 |
| MCP endpoint | apps/api |
/mcp |
3000 |
| Runner API | apps/runner |
/ |
3003 |
| Preview proxy API | apps/proxy |
proxy.<domain>, <port>-<box>.proxy.<domain> |
4000 |
| Registry proxy API | apps/image-service |
/v2 |
4100 |
| Telemetry collector API | apps/otel-collector |
OTLP/HTTP and health listeners | 4318, 13133, 13132 |
| Local development stack | apps/infra-local |
http://localhost:28080 |
local host ports only |
| Container-based local environment | apps/scripts |
containers on 5556, 5432, 6379, 5001; apps on 3000, 3001, 4000, 8080 |
local host ports only |
| Externally served APIs | apps/dashboard, apps/api |
discovered via GET /api/config |
— |
| Services without a first-party API | remaining apps | — | — |
Ports are the values used in deployments. Service ports and the collector's
OTLP and HTTP health ports come from
settings.ts; the collector's gRPC health port
comes from its own config.yaml. Where an
in-code default differs, the section notes it.
These read-only views require system administrator authorization. Registration filters,
creator semantics and live pagination are documented in the API README.
Existing JWT organization reads and the Admin-key by-box-id lookup already return creation
fields; the existing admin routes now add cross-organization date filtering, coverage of
organizations without Boxes, and opt-in sections. Registration fields are always returned;
usage and activation are read only when explicitly included. Upgrade all producers before
enabling consumers of this contract.
| Method | Path | Behavior |
|---|---|---|
GET |
/api/admin/organizations |
Lists registration metadata, counts and impact without usage reads. |
GET |
/api/admin/organizations/{organizationId} |
Flat registration, member, Box and impact detail; include=usage,activation selects optional sections. |
Service: apps/api · Base path: /api · Port: 3000 in
deployments (PORT environment variable; no in-code default)
This is the hosted product API used by the dashboard, SDKs, CLI, runners, and internal services. Authentication and authorization vary by route; the health and configuration discovery routes are public, while resource routes enforce user, organization, runner, or admin credentials. The static trees this process serves and the events it emits are catalogued below alongside its routes.
Discovery and API keys · 7 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/config |
Returns browser/client configuration, including OIDC settings. |
GET |
/api/api-keys |
Lists API keys visible to the caller. |
POST |
/api/api-keys |
Creates an API key. |
GET |
/api/api-keys/current |
Returns details for the API key authenticating the request. |
GET |
/api/api-keys/{name} |
Gets an API key by name. |
DELETE |
/api/api-keys/{name} |
Deletes the caller's API key by name. |
DELETE |
/api/api-keys/{userId}/{name} |
Deletes a named API key belonging to a specified user. |
Key creation takes an explicit permissions array. When authenticated with an API key,
the requested permissions must be a subset of that key's permissions, including for
owners and system admins; broader grants return 403 before a key is created.
The user's existing grantability checks still apply. An owner using a user JWT
can create a key with any supported permission.
Both DELETE routes require write:members when authenticated with an API key,
including owner/admin keys and revocation of the calling key itself. A
read:members key cannot revoke any key. User JWTs retain their existing access:
members can revoke their own keys; owners and admins can also revoke other users'
keys within the selected organization. The scope does not bypass these user or
organization boundaries.
Users and authentication · 12 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/users/me |
Returns the authenticated user. |
GET |
/api/users |
Lists users. |
POST |
/api/users |
Creates a user. |
GET |
/api/users/{id} |
Gets a user by ID. |
POST |
/api/users/{id}/regenerate-key-pair |
Regenerates a user's key pair. |
GET |
/api/users/account-providers |
Lists account providers available for linking. |
POST |
/api/users/linked-accounts |
Links an external account to the user. |
DELETE |
/api/users/linked-accounts/{provider}/{providerUserId} |
Unlinks an external account. |
POST |
/api/users/mfa/sms/enroll |
Enrolls the user in SMS MFA. |
GET |
/api/auth/end-session |
Starts OIDC RP-initiated logout. |
POST |
/api/auth/link/adopt |
Moves a linked account's data for the Action. |
GET |
/api/auth/link/status |
Tells the Action whether BoxLite knows a user. |
Organizations, membership, and invitations · 24 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/organizations |
Lists organizations available to the caller. |
POST |
/api/organizations |
Creates an organization. |
GET |
/api/organizations/{organizationId} |
Gets an organization by ID. |
GET |
/api/organizations/{organizationId}/referral-code |
Gets or initializes the organization's invitation code. |
GET |
/api/organizations/{organizationId}/concurrency |
Gets a bounded concurrency timeline from usage periods. |
DELETE |
/api/organizations/{organizationId} |
Deletes an organization. |
PATCH |
/api/organizations/{organizationId}/name |
Changes an organization's name. |
PATCH |
/api/organizations/{organizationId}/default-region |
Sets the organization's default region. |
POST |
/api/organizations/{organizationId}/leave |
Removes the caller and revokes their API keys here. |
POST |
/api/organizations/{organizationId}/suspend |
Suspends the organization. |
POST |
/api/organizations/{organizationId}/unsuspend |
Restores a suspended organization. |
GET |
/api/organizations/by-box-id/{boxId} |
Resolves the organization that owns a box. |
POST |
/api/organizations/{organizationId}/box-default-limited-network-egress |
Changes the default limited-egress policy for new boxes. |
PUT |
/api/organizations/{organizationId}/experimental-config |
Replaces the organization's experimental configuration. |
GET |
/api/organizations/{organizationId}/users |
Lists organization members. |
POST |
/api/organizations/{organizationId}/users/{userId}/access |
Changes access; refuses to demote a default-org creator. |
DELETE |
/api/organizations/{organizationId}/users/{userId} |
Removes a member and revokes their API keys here. |
GET |
/api/organizations/invitations |
Lists invitations addressed to the caller. |
GET |
/api/organizations/invitations/count |
Counts invitations addressed to the caller. |
POST |
/api/organizations/invitations/{invitationId}/accept |
Accepts an organization invitation. |
POST |
/api/organizations/invitations/{invitationId}/decline |
Declines an organization invitation. |
GET |
/api/organizations/{organizationId}/invitations |
Lists pending invitations, including expired ones. |
POST |
/api/organizations/{organizationId}/invitations |
Creates an organization invitation. |
PUT |
/api/organizations/{organizationId}/invitations/{invitationId} |
Changes a pending invitation. |
POST |
/api/organizations/{organizationId}/invitations/{invitationId}/cancel |
Cancels a pending invitation, even after expiry. |
Roles and regions · 10 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/organizations/{organizationId}/roles |
Lists organization roles. |
POST |
/api/organizations/{organizationId}/roles |
Creates an organization role. |
PUT |
/api/organizations/{organizationId}/roles/{roleId} |
Updates an organization role. |
DELETE |
/api/organizations/{organizationId}/roles/{roleId} |
Deletes an organization role. |
POST |
/api/regions |
Creates a region. |
GET |
/api/regions/{id} |
Gets a region by ID. |
PATCH |
/api/regions/{id} |
Updates a region's configuration. |
DELETE |
/api/regions/{id} |
Deletes a region. |
POST |
/api/regions/{id}/regenerate-proxy-api-key |
Regenerates the region proxy's API key. |
GET |
/api/shared-regions |
Lists shared regions. |
Boxes and preview access · 19 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/box |
Lists all boxes visible to the caller. |
GET |
/api/box/paginated |
Lists boxes with pagination. |
GET |
/api/box/for-runner |
Lists boxes assigned to the authenticated runner. |
GET |
/api/box/{boxIdOrName} |
Gets a box by ID or name. |
POST |
/api/box/{boxIdOrName}/recover |
Requests recovery of a box in an error state. |
PUT |
/api/box/{boxIdOrName}/labels |
Replaces a box's labels. |
PUT |
/api/box/{boxId}/state |
Updates the control plane's recorded box state. |
POST |
/api/box/{boxId}/last-activity |
Records the box's latest activity time. |
POST |
/api/box/{boxIdOrName}/autostop/{interval} |
Sets the box auto-stop interval. |
POST |
/api/box/{boxIdOrName}/autodelete/{interval} |
Sets the box auto-delete interval. |
GET |
/api/box/{boxId}/toolbox-proxy-url |
Returns the box toolbox proxy URL. |
POST |
/api/box/{boxIdOrName}/attach-tickets |
Issues a 30-second, single-use ticket for one /v1 attach upgrade. |
GET |
/api/preview/{boxId}/inbound |
Reports a box's inbound mode (enabled or disabled) to the proxy. |
GET |
/api/preview/{boxId}/tunnels/{port} |
Checks an active public tunnel for the proxy. |
POST |
/api/preview/{boxId}/tunnels/{port}/access |
Proxy-only active tunnel policy, token verification and browser-user revalidation; returns no token verifier. |
GET |
/api/preview/{boxId}/tunnels/{port}/session |
Checks a user JWT and returns the tunnel grant and user ID for browser login. |
POST |
/api/preview/tunnel-handoff |
Proxy-only issue or single-use consumption of a 30-second browser login handoff. |
GET |
/api/preview/domains/{hostname} |
Resolves a platform hostname to {boxId, port} for the proxy; 404 unless it may be forwarded now. |
POST |
/api/preview/{boxId}/ensure-ready |
Wakes a stopped box for the proxy, by auto-resume rules. |
GET |
/api/preview/{boxId}/access |
Checks whether the caller may preview a box. |
Volumes and object storage · 6 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/volumes |
Lists volumes visible to the caller. |
POST |
/api/volumes |
Creates a volume. |
GET |
/api/volumes/{volumeId} |
Gets a volume by ID. |
DELETE |
/api/volumes/{volumeId} |
Deletes a volume. |
GET |
/api/volumes/by-name/{name} |
Gets a volume by name. |
GET |
/api/object-storage/push-access |
Returns temporary credentials for pushing objects. |
Image catalog · 6 routes
upstreamMissingAt records when a pull first found a version missing upstream.
null means no pull has recorded that observation; it does not confirm current
registry availability.
List and detail responses expose aliases only for READY versions, matching the versions a box can resolve. Aliases of non-ready versions remain stored but are omitted from those responses.
| Method | Path | What it does |
|---|---|---|
GET |
/api/images |
Lists the organization's images together with the operator's curated set. |
GET |
/api/images/usage |
Counts held images against the limit and sums the sizes their versions declared. |
GET |
/api/images/{idOrRef} |
Gets an image, its versions with their aliases, last-use and upstream-missing timestamps, and the last 100 alias events. |
POST |
/api/images/{idOrRef}/aliases |
Creates an alias for a ready version; existing names return 409, even for the same target. |
DELETE |
/api/images/{idOrRef}/aliases/{alias} |
Deletes only the alias; versions and existing boxes remain. |
DELETE |
/api/images/{idOrRef} |
Removes the catalog entry, not the bytes, unless a box can still boot from it (409 lists the boxes). |
Registry credentials · 4 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/images/registries |
Lists the organization's private registry logins, without passwords. |
POST |
/api/images/registries |
Adds a login; the password is stored where the API cannot read it and never returned. |
PUT |
/api/images/registries/{id} |
Replaces username/password while preserving scope and dependent Boxes; requires write:registries. |
DELETE |
/api/images/registries/{id} |
Removes a login, unless a box still pulls through it (409 lists the boxes). |
Update requires both username and password (or token); host/prefix and other fields are rejected.
The response is the same password-free credential as creation; missing credentials in the authenticated organization return 404.
Concurrent mutations can return 409; retry after the other request finishes.
See rotation ordering and recovery for secret retirement and cache limits.
Runners and jobs · 16 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/runners |
Lists runners. |
POST |
/api/runners |
Registers a runner. |
GET |
/api/runners/me |
Returns the authenticated runner's identity and configuration. |
POST |
/api/runners/me/cpu-incidents |
Internal runner-only CPU incident ingestion; excluded from customer OpenAPI and SDKs. |
POST |
/api/runners/me/memory-incidents |
Internal runner-only top-five memory snapshot ingestion; excluded from customer OpenAPI and SDKs. |
GET |
/api/runners/me/images/registry-credentials |
Internal registry-proxy lookup; excluded from customer OpenAPI and SDKs. |
GET |
/api/runners/by-box/{boxId} |
Resolves the runner assigned to a box. |
GET |
/api/runners/{id} |
Gets a runner by ID. |
GET |
/api/runners/{id}/full |
Gets a runner with its full related data. |
DELETE |
/api/runners/{id} |
Deletes a runner. |
PATCH |
/api/runners/{id}/scheduling |
Enables or disables scheduling onto a runner. |
PATCH |
/api/runners/{id}/draining |
Enables or disables runner draining. |
POST |
/api/runners/healthcheck |
Records or checks runner health. |
GET |
/api/jobs |
Lists jobs assigned to the authenticated runner. |
GET |
/api/jobs/poll |
Long-polls for work assigned to the runner. |
GET |
/api/jobs/{jobId} |
Gets a job by ID. |
POST |
/api/jobs/{jobId}/status |
Reports job progress, success, or failure. |
Administration · 7 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/admin/runner-cpu-incidents |
Queries CPU incident evidence by capture completion time range for backoffice operations. |
GET |
/api/admin/runner-memory-incidents |
Queries top-five memory snapshots by host sample time for backoffice operations. |
GET |
/api/admin/runners |
Lists runners using admin authorization. |
POST |
/api/admin/runners |
Registers a runner as an administrator. |
GET |
/api/admin/runners/{id} |
Gets a runner by ID as an administrator. |
DELETE |
/api/admin/runners/{id} |
Deletes a runner as an administrator. |
PATCH |
/api/admin/runners/{id}/scheduling |
Changes runner scheduling as an administrator. |
POST |
/api/admin/box/{boxId}/recover |
Requests box recovery as an administrator. |
Webhooks and audit · 8 routes
| Method | Path | What it does |
|---|---|---|
POST |
/api/webhooks/organizations/{organizationId}/app-portal-access |
Creates access to the organization's Svix consumer portal. |
POST |
/api/webhooks/organizations/{organizationId}/send |
Sends a webhook event to the organization. |
GET |
/api/webhooks/organizations/{organizationId}/messages/{messageId}/attempts |
Lists delivery attempts for a webhook message. |
GET |
/api/webhooks/status |
Reports webhook service status. |
GET |
/api/webhooks/organizations/{organizationId}/initialization-status |
Reports whether organization webhooks are initialized. |
POST |
/api/webhooks/organizations/{organizationId}/initialize |
Initializes webhooks for an organization. |
GET |
/api/audit |
Lists audit records available to the caller. |
GET |
/api/audit/organizations/{organizationId} |
Lists one organization's audit records. |
Box telemetry · 5 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/box/{boxId}/telemetry/logs |
Queries logs for a box. |
GET |
/api/box/{boxId}/telemetry/traces |
Queries traces for a box. |
GET |
/api/box/{boxId}/telemetry/traces/{traceId} |
Gets spans for one trace. |
GET |
/api/box/{boxId}/telemetry/metrics |
Queries metrics for a box. |
GET |
/api/organizations/otel-config/by-box-auth-token/{authToken} |
Resolves a box token to its organization telemetry export configuration. |
Health and API documentation · 5 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/health |
Reports process liveness. |
GET |
/api/health/ready |
Checks authenticated readiness, including Postgres and Redis. |
GET |
/api |
Serves the interactive Swagger UI. |
GET |
/api-json |
Returns the generated product OpenAPI document as JSON. |
GET |
/api-yaml |
Returns the generated product OpenAPI document as YAML. |
Static asset trees · 2 trees
| Method | Path | What it does |
|---|---|---|
GET |
/runner-amd64 |
Serves runner-amd64 from the deployment root, when that file is there. |
GET |
/ |
Serves the dashboard single-page app and its hashed /assets/* bundles. |
Both trees are registered by ServeStaticModule in
app.module.ts and exclude /api/{*path}, so they
never shadow the routes above. The dashboard tree applies a content-addressed
cache policy from serve-static-cache.ts:
hashed /assets/* files are immutable for a year, HTML is no-cache, and
everything else must revalidate.
The file that route resolves to is the runner binary
project.json builds at dist/apps/runner-amd64, so
the path serves something only in a full local build: the deployed image stages
just dist/apps/api and dist/apps/dashboard
(Dockerfile). No in-repo client fetches it either — runners
take their binary from the artifact source resolved in
source.ts, which defaults to a published
release fetched over HTTPS.
Realtime and asynchronous interfaces · Socket.IO, 9 events, 4 webhook events, 2 Kafka topics
| Protocol | Path, topic, or event | What it does |
|---|---|---|
| Socket.IO over WebSocket | /api/socket.io/ |
Authenticates a JWT or API key and joins the user room, plus the organization room for members only; removal ejects the member. |
| Socket.IO event | box.created |
Notifies an organization that a box was created. |
| Socket.IO event | box.state.updated |
Notifies an organization that a box's observed state changed. |
| Socket.IO event | box.desired-state.updated |
Notifies an organization that a box's desired state changed. |
| Socket.IO event | volume.created |
Notifies an organization that a volume was created. |
| Socket.IO event | volume.state.updated |
Notifies an organization that a volume's state changed. |
| Socket.IO event | volume.lastUsedAt.updated |
Notifies an organization that a volume's last-used time changed. |
| Socket.IO event | runner.created |
Notifies an organization that a runner was registered. |
| Socket.IO event | runner.state.updated |
Notifies an organization that a runner's state changed. |
| Socket.IO event | runner.unschedulable.updated |
Notifies an organization that runner scheduling availability changed. |
| Webhook event (Svix) | box.created |
Delivers a signed box-created event to the organization's subscribed endpoints. |
| Webhook event (Svix) | box.state.updated |
Delivers a box observed-state change to the organization's subscribed endpoints. |
| Webhook event (Svix) | volume.created |
Delivers a volume-created event to the organization's subscribed endpoints. |
| Webhook event (Svix) | volume.state.updated |
Delivers a volume state change to the organization's subscribed endpoints. |
| Kafka event | audit-logs topic |
When the API worker and Kafka are enabled, consumes audit-log events and persists them, with bounded retries and dead-letter handling. |
| Kafka event | audit-logs.dlq topic |
Receives audit-log events that exhausted their retries, with the failure reason and retry count attached. |
Socket.IO and webhook events share four names but not their delivery: Socket.IO
broadcasts to the organization's room over the authenticated WebSocket, while
webhook events are delivered as signed HTTP requests through Svix to endpoints
the organization manages in its consumer portal. Payload schemas are published
in a webhooks block that
openapi-webhooks.ts adds to the generated
openapi.3.1.0.json only — not to the checked-in openapi.yaml named above.
The template.* values remain in the WebhookEvent enum but nothing emits
them.
Service: apps/api · Base path: /api/v1 · Port: 3000 in
deployments (shared with the control-plane API)
Most resource paths accept both an unprefixed form and an organization-prefixed
form. The tables write that as [/{prefix}]; clients can discover their prefix
through GET /api/v1/me. Except for configuration discovery, these routes use
combined bearer authentication, then organization authorization.
Image and registry management routes also consume the organization's authenticated
request quota, shared with /api/images, /api/images/registries, and other routes using
that quota. Canonical and prefixed SDK paths use the same counter. After the quota
is exhausted, requests return HTTP 429 with Retry-After-authenticated before
catalog changes or registry credential operations run; switching API surfaces
does not reset it.
Resource permissions are enforced per route. A box mutation — create, start,
stop, exec, signal, resize, execution kill, file transfer, WebSocket attach, an
attach ticket, and the network routes — requires write:boxes; deleting a box requires
delete:boxes. Box reads (list, get, head, execution status, metrics) carry no
permission and are reached by any member, because the model has no read:boxes.
Volume routes require read:volumes, write:volumes, or delete:volumes to
match, image routes read:images, write:images, or delete:images, and
registry login routes read:registries, write:registries, or
delete:registries. Attaching a managed volume while creating a box is authorized
independently of write:boxes: the caller must also hold read:volumes and
write:volumes, so a box-only credential cannot reach another's volume data
(GHSA-2qqv-7cwv-mj8h). A system admin and an interactive owner without an API key
are not permission-bounded; every API key is bounded by the permissions it
carries. The WebSocket /attach upgrade enforces write:boxes inline — an
upgrade never passes through the organization guard — using the same admin and
owner exemptions, so it resolves the caller's system role just as the HTTP path
does. A browser, which cannot set Authorization on a WebSocket, instead sends a
?ticket= bought from POST /api/box/{boxIdOrName}/attach-tickets. Spending it
re-checks the ticket's credential the same way:
the API key that bought it, which must still exist and be unexpired, or else
its user. The attach URL may omit {prefix}; the ticket already names the organization.
GET /api/v1/me can return path_prefix: null for an authenticated user with no
organization membership.
Discovery · 2 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/v1/config |
Reports which optional portable API capabilities are enabled. |
GET |
/api/v1/me |
Returns the calling principal, path prefix, scopes, and credential expiry. |
Box lifecycle · 7 routes
| Method | Path | What it does |
|---|---|---|
POST |
/api/v1[/{prefix}]/boxes |
Creates a box and waits for it to start. |
GET |
/api/v1[/{prefix}]/boxes |
Lists boxes in the organization. |
GET |
/api/v1[/{prefix}]/boxes/{boxId} |
Gets a box by ID or name. |
HEAD |
/api/v1[/{prefix}]/boxes/{boxId} |
Checks whether a box exists. |
DELETE |
/api/v1[/{prefix}]/boxes/{boxId} |
Destroys a box. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/start |
Starts a box and waits until it is running. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/stop |
Stops a box. |
Execution, files, metrics, networking, and SSH · 16 routes
| Method | Path | What it does |
|---|---|---|
POST |
/api/v1[/{prefix}]/boxes/{boxId}/exec |
Starts a command execution through the assigned runner. |
GET |
/api/v1[/{prefix}]/boxes/{boxId}/executions |
Lists live guest executions, including the main session, without resuming the box or recording activity. |
GET |
/api/v1[/{prefix}]/boxes/{boxId}/executions/{execId} |
Gets execution status and exit information. |
DELETE |
/api/v1[/{prefix}]/boxes/{boxId}/executions/{execId} |
Kills an execution. |
WS |
/api/v1[/{prefix}]/boxes/{boxId}/executions/{execId}/attach |
Attaches a bidirectional WebSocket to an execution; a browser authenticates with ?ticket=. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/executions/{execId}/signal |
Sends a signal to an execution. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/executions/{execId}/resize |
Resizes an execution's terminal. |
PUT |
/api/v1[/{prefix}]/boxes/{boxId}/files?path={path} |
Uploads a tar stream into a box path. |
GET |
/api/v1[/{prefix}]/boxes/{boxId}/files?path={path} |
Downloads a box path as a tar stream. |
GET |
/api/v1[/{prefix}]/boxes/{boxId}/metrics |
Returns metrics for one box without marking it active. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/network/tunnel?port={port} |
Prepares a tunnel with optional isPublic (default true); returns uri, isPublic, token. Private tokens are complete once, then masked. |
DELETE |
/api/v1[/{prefix}]/boxes/{boxId}/network/tunnel?port={port} |
Revokes either access mode; 204, idempotent. |
GET |
/api/v1[/{prefix}]/domains |
Lists full hostnames, URLs, typed targets and the organization's allowance; no short name field. |
PUT |
/api/v1[/{prefix}]/domains/{domain}/target |
Routes an owned full hostname to a box port using {"type":"box","box":"<id-or-name>","port":3000}; replaces any existing binding without declaring the port public. |
DELETE |
/api/v1[/{prefix}]/domains/{domain}/target |
Requires the full held hostname; stops routing it and keeps ownership; 204, idempotent. |
PUT |
/api/v1[/{prefix}]/boxes/{boxId}/network/inbound |
Enables or disables a box's inbound access. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/ssh |
Replaces guest SSH configuration once; counts as activity and may auto-resume a box. |
POST |
/api/v1[/{prefix}]/boxes/{boxId}/ssh/enable |
Enables saved SSH configuration on a running box; no auto-resume or activity. |
GET |
/api/v1[/{prefix}]/boxes/{boxId}/ssh |
Reads guest SSH status without resuming the box or counting as activity. |
DELETE |
/api/v1[/{prefix}]/boxes/{boxId}/ssh |
Disables guest SSH on a running box; otherwise 409. No auto-resume or activity. |
Execution inventory is an organization-member read, like execution status; it
does not require write:boxes. Box lookup remains scoped to that organization.
The response is { "executions": [...] }, with execution_id, argv,
started_at (Unix milliseconds), tty, and is_main_session on each entry.
Use the returned guest ID with the status, attach, signal, resize, and kill
routes, including for the main session; terminating main can stop the box.
Listing a stopped box returns an empty inventory and never starts it.
The exec, signal, resize, files, and metrics handlers are registered as catch-all reverse proxies to the box's assigned runner; the methods shown are the portable contract, which the runner's own route registration enforces.
SSH configuration uses the reported lifecycle state for admission: a stored
stopped or unknown state with desired state started is treated as starting
or creating, respectively. With auto-resume enabled, configuration waits for
that start, including for boxes with an explicit cmd or entrypoint. Before
submitting a new start, the API rechecks SSH startup restrictions against the
current box state under its lifecycle lock. The API waits for readiness before
forwarding configuration; the runner checks that the box is running and returns
409 invalid_state if that check fails.
Volumes · 4 routes
| Method | Path | What it does |
|---|---|---|
POST |
/api/v1[/{prefix}]/volumes |
Creates a volume and waits until it is ready. |
GET |
/api/v1[/{prefix}]/volumes |
Lists volumes in the organization. |
GET |
/api/v1[/{prefix}]/volumes/{volumeId} |
Gets a volume by ID. |
DELETE |
/api/v1[/{prefix}]/volumes/{volumeId}?force={boolean} |
Deletes a volume, optionally forcing removal. |
Images · 6 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/v1[/{prefix}]/images |
Lists one row per reference the organization can boot from; curated images are left out. |
GET |
/api/v1[/{prefix}]/images/usage |
Counts held images against the limit and sums the sizes their versions declared. |
GET |
/api/v1[/{prefix}]/images/{name} |
Gets an image: its versions with their aliases, last use and upstream state, and the last 100 alias events. |
POST |
/api/v1[/{prefix}]/images/{name}/aliases |
Creates an alias from alias and digest; existing names return already_exists. |
DELETE |
/api/v1[/{prefix}]/images/{name}/aliases/{alias} |
Removes only the alias; needs write:images. |
DELETE |
/api/v1[/{prefix}]/images/{name} |
Removes the catalog entry, not the bytes, unless a box can still boot from it. |
The same catalog as /api/images, in the box API's snake_case shapes and with
versions named by digest, for SDK clients.
Registries · 3 routes
| Method | Path | What it does |
|---|---|---|
GET |
/api/v1[/{prefix}]/images/registries |
Lists the organization's private registry logins, without passwords. |
POST |
/api/v1[/{prefix}]/images/registries |
Adds a login; a second one for the same registry and prefix is already_exists. |
DELETE |
/api/v1[/{prefix}]/images/registries/{id} |
Removes a login, unless a box still pulls through it (409 lists the boxes). |
The same logins as /api/images/registries, in the box API's snake_case shapes, for
SDK clients.
Credential deletion and private-image box persistence coordinate in PostgreSQL. If they overlap, the contending request returns 409 and can be retried. A box whose resolved proxy reference has lost its credential is also rejected before it is saved. Once a dependent box commits, deletion returns 409 with its ID. The dependency check reads candidate credentials once for the organization and registry host, then matches each box in memory using the longest repository prefix.
Members and invitations · 5 routes
Contract: openapi/admin.openapi.yaml. This cloud-only
Admin API manages an organization's users and invitations. Ordinary scoped API keys
and user JWTs can call it; a platform administrator account is not required.
Dedicated Admin SDK methods are not yet available. Existing Cloud API SDK methods
continue to manage organizations and invitations.
| Method | Path | Required API-key scope | Membership rule |
|---|---|---|---|
GET |
/api/v1/organizations/users |
read:members |
Member |
DELETE |
/api/v1/organizations/users/{user_id} |
write:members |
Owner |
GET |
/api/v1/organizations/invites |
read:members |
Member |
POST |
/api/v1/organizations/invites |
write:members |
Owner |
DELETE |
/api/v1/organizations/invites/{invite_id} |
write:members |
Owner |
The key selects its organization. JWT callers select an organization with
X-BoxLite-Organization-ID, or use their default organization. Read and write
scopes are independent; owner and platform-admin keys still need the declared
scope. For example, create a restricted key through the Cloud API/SDK with
{"name":"member-reader","permissions":["read:members"]}. It can list users and
invites but cannot invite users or delegate a key with write:members.
Console owner-created keys default to both member scopes.
The unmerged Tenant routes and scope names are replaced directly. Previously stored tenant grants migrate to the equivalent member grants; keys without those grants do not gain them. Removing a user revokes their organization keys. The last owner and a user's personal/default organization membership remain protected.
Both lists accept pageSize (1–1000, default 100) and pageToken. Responses contain
users or invites, plus next_page_token when more records exist. Treat tokens
as opaque and return them unchanged; they are bound to the organization and
resource and never grant access. Lists use ascending resource IDs.
Users have type: "user", id (the same as /v1/me's sub), email, name,
role, and added_at. Email is null when absent or the user record is unavailable.
Invites have type: "invite", id, email, role, status, added_at, and
expires_at. Lists include every state; pending invitations past their deadline
appear as expired. Expired pending invitations can be cancelled but not accepted.
Create an invitation with {"email":"teammate@example.com","role":"owner"}.
The role is required and currently only owner is accepted. Optional expires_at
must be in the future on initial creation; omission uses
ORGANIZATION_INVITATION_TTL_SECONDS, default seven days. Unknown request fields
are rejected. Clients should tolerate additional response fields.
An optional UUID Idempotency-Key makes create-invite retries return the original
201 response for 24 hours, scoped to the organization and acting user. A different
payload with the same key returns 422; a concurrent in-progress request returns
409 and may be retried. Replay works after cancellation or expiry and does not
send another email. This guarantee applies to create-invite only; it does not
change Box /v1/config capabilities. The email event is not a transactional outbox:
a process crash after commit can still lose notification delivery.
Admin errors use the shared Box shape, for example
{"error":{"message":"Forbidden resource","type":"AuthError","code":"permission_denied"}}.
All operations may return 429; respect Retry-After when present. Console Cloud
API errors retain their existing flat shape.
Invitees accept through the JWT-only Console endpoint
/api/organizations/invitations/{invitationId}/accept. Acceptance requires the
invited email and does not require member scopes or prior membership. Email links
resolve /dashboard/user/invitations against DASHBOARD_URL and accept after
sign-in. See invitation link lifetime.
Backoffice uses GET /api/admin/runner-cpu-incidents with an administrator API key
or JWT. Ordinary users and runner credentials receive 403; absent/invalid credentials
receive 401. Responses use Cache-Control: private, no-store.
| Query | Contract |
|---|---|
from |
Required RFC 3339 timestamp with timezone, at most milliseconds. Inclusive lower bound on windowEndedAt. |
to |
Required RFC 3339 timestamp with timezone, at most milliseconds. Exclusive upper bound on windowEndedAt; must be after from. |
limit |
Integer 1–200; default 50. First N rows in ID ascending order after per-Box maximum selection. |
runnerId |
Optional UUID filter. |
curl --get "$BOXLITE_API_URL/api/admin/runner-cpu-incidents" \
--header "Authorization: Bearer $BOXLITE_ADMIN_API_KEY" \
--data-urlencode "from=2026-10-29T00:00:00Z" \
--data-urlencode "to=2026-10-30T00:00:00Z" \
--data-urlencode "limit=100"{
"items": [{
"id": "42", "runnerId": "11111111-1111-4111-8111-111111111111",
"eventId": "22222222-2222-4222-8222-222222222222",
"highCpuSince": "2026-10-29T00:00:00.000Z",
"windowStartedAt": "2026-10-29T00:05:00.000Z",
"windowEndedAt": "2026-10-29T00:07:00.000Z",
"runnerCpuPercent": 85, "boxId": "example-box", "image": "ubuntu:24.04",
"cpuSeconds": 240, "samplingComplete": true,
"receivedAt": "2026-10-29T00:12:01.000Z"
}]
}Ranking queries have a five-second database statement timeout, including lock waits. A timeout fails the request without partial results. Valid time ranges remain unrestricted; a timed-out query can be retried with a narrower range. The setting is transaction-local and does not affect ingestion or pooled sessions.
The range is [from,to) on capture completion time, not high-load start or server
receipt time. Adjacent ranges therefore do not share boundary rows. After the time
and optional Runner filters, each non-null boxId appears once, using the row
with the largest cpuSeconds; equal values retain the smaller numeric row ID.
Without a Runner filter, the same Box is deduplicated across Runners. All fields,
including the image and capture timestamps, come from that winning row.
Rows with boxId=null remain separate observations. Only after this selection are
results ordered by ID ascending and capped at limit; additional selected rows
are not returned. There is no pagination. An empty result is {"items":[]}. IDs remain
decimal strings to preserve bigint precision; ID order need not match completion
or commit order. Upload retries can add records to older ranges. Repeating a query
still returns only the first N selected rows; this endpoint is a per-Box maximum
view, not a complete incident history. Consumers must recheck the box's
current state before operations: evidence persists after migration or deletion.
cpuSeconds / window duration in seconds * 100 gives average box CPU percent
on a one-core scale, which can exceed 100%. samplingComplete=false means the
ranking uses incomplete measurements; boxId=null means no positive CPU delta
was measurable. image is the winner's exact BoxInfo.Image string from its last
successful counter sample in the window, retained even if that box subsequently
exits. No winner or an older report without image produces image=null; an empty
string is preserved. This is the supplied image reference, not a resolved digest.
There is no acknowledgement or automatic box operation.
Runner ingestion is POST /api/runners/me/cpu-incidents, authenticated by the
runner's existing token. Its body is the example row without id, runnerId
and receivedAt. Valid first uploads and retries return 204. The API derives
runnerId from credentials, requires more than 80% CPU and at least five minutes
between highCpuSince and windowStartedAt, validates the capture and counters, and ignores
duplicate event IDs from the same runner. Malformed bodies/queries return 400.
The optional image accepts a string or null; it must be absent/null when boxId
is null. It travels in this same report without another runner API query.
This internal route is excluded from customer OpenAPI and generated SDKs;
the admin route is described by Nest Swagger.
Deploy the pre-deploy migration, then the API, then runners. See the runner monitoring contract for sampling, retry and restart limitations and the table schema for persistence guarantees.
Each minute, a host memory reading strictly above 80% triggers an immediate scan of running Boxes. The Runner reports the five largest readable memory usages, or fewer when fewer are available. There is no five-minute qualification or two-minute capture window. CPU incidents retain their existing policy.
POST /api/runners/me/memory-incidents uses the Runner token and returns 204
for first ingestion and retries. The server derives runnerId from credentials;
duplicate (runnerId,eventId) reports leave the first snapshot unchanged.
The route is internal and excluded from customer OpenAPI and SDKs.
{
"eventId": "22222222-2222-4222-8222-222222222222",
"sampledAt": "2026-11-05T00:01:00.000Z",
"runnerMemoryPercent": 87.5,
"boxes": [
{ "boxId": "box-a", "image": "ubuntu:24.04", "memoryBytes": "4294967296" },
{ "boxId": "box-b", "image": "python:3.13", "memoryBytes": "3221225472" }
],
"samplingComplete": true
}boxes is required and has at most five distinct business Box IDs, ordered by
bytes descending and then ID ascending. Each item requires a string image
(empty is preserved) and canonical decimal-string memoryBytes between zero
and 9223372036854775807. JSON numbers are rejected to prevent precision loss.
An empty array means no Box could be measured or no Boxes were running.
samplingComplete=false means enumeration or at least one reading failed;
the reported ranking then covers only successful observations. Unknown fields,
invalid nested items, duplicate IDs, incorrect ordering, invalid dates, and host
percentages outside (80,100] return 400.
Administrators query GET /api/admin/runner-memory-incidents using an admin API
key or JWT. Ordinary users and Runner credentials receive 403; missing or invalid
credentials receive 401. Responses use Cache-Control: private, no-store.
| Query | Contract |
|---|---|
from |
Required timezone-qualified timestamp, inclusive lower bound on sampledAt. |
to |
Required timezone-qualified timestamp, exclusive upper bound on sampledAt; after from. |
runnerId |
Optional UUID filter. |
limit |
Integer 1–200, default 50; first N snapshots in ID ascending order after per-Box maximum selection. |
Timestamps accept at most millisecond precision. Results are {"items":[...]};
each item adds decimal-string id, authenticated runnerId, and server
receivedAt to the snapshot above. Across the full time range and optional Runner
filter, each boxId appears only in the snapshot containing its largest
memoryBytes, compared as an exact integer. Equal values retain the smaller
numeric snapshot ID. Without a Runner filter, deduplication spans Runners.
Each returned boxes array contains only the winning entries from that snapshot,
in their original order, with their original images and snapshot metadata.
Snapshots losing all entries are omitted; originally empty snapshots remain.
Then snapshots are ordered by ID and capped at limit. The limit counts snapshots,
not individual Boxes; one snapshot can still contain up to five Boxes.
Stored reports remain intact. Empty results are {"items":[]}.
As with CPU queries, ranking has a five-second transaction-local database statement
timeout, including lock waits. A timeout fails without partial results; retry with
a narrower range if necessary. Ingestion and other pooled queries are unaffected.
There is no pagination; repeated queries with the same limit do not enumerate
additional rows. Late retries retain their original sampledAt, so historical
ranges can gain records. Box deletion does not remove evidence.
Deploy the pre-deploy table migration, then the API, then Runners. See the sampling and retry contract for accounting scope and delivery limitations.
Service: apps/api · Path: /mcp · Port: 3000 in deployments
(shared with the control-plane API)
The endpoint and its OAuth metadata sit outside the api global prefix
(MCP_ROOT_ROUTES), so the URL a client
connects to is also the OAuth resource that RFC 9728 discovery checks. The GCP URL
map passes both paths through unchanged, and /api/mcp is not served.
A Model Context Protocol server, served from mcp over
Streamable HTTP. It speaks both protocol eras on one endpoint: the 2025
initialize handshake, served statelessly, and the session-less 2026-07-28
revision. Every POST is answered with one JSON body; subscriptions/listen is
refused with -32601. A request that carries an Origin header is refused with
403, because MCP hosts call from servers and CLIs, not browsers.
claude mcp add --transport http boxlite https://<api-host>/mcp \
--header "Authorization: Bearer $BOXLITE_API_KEY"
On a stage with MCP_RESOURCE_URL set and its tenant configured
(MCP client sign-in), leave
out --header: the client opens a browser sign-in instead.
Credentials. A user API key, or an OAuth token whose audience is
MCP_RESOURCE_URL. Dashboard tokens are refused, as are the API keys of machine
principals (proxy, runner, collectors); without MCP_RESOURCE_URL every OAuth token
is refused. A 401 carries
WWW-Authenticate: Bearer resource_metadata=... naming the RFC 9728 document at
/.well-known/oauth-protected-resource/mcp, which lists the API's OIDC issuer
as the authorization server; it is published only when MCP_RESOURCE_URL is set,
and otherwise answers 404 (no /.well-known/ path falls back to the dashboard page).
OAuth callers. McpAuthGuard turns an OAuth
token into an API-key-shaped caller, so the REST guards apply their API-key rules
unchanged:
| Property | Value |
|---|---|
| Organization | The user's default organization, read on every request; changing the default moves existing tokens with it |
| System role | Always USER, so no admin exemption applies to a token |
| Permissions | The token's scope limited to MCP_OAUTH_SCOPES (box, volume, image and template permissions), then to what the user's role holds; an owner holds all |
McpScopeGuard checks every tools/call from
an OAuth caller before it runs, for direct tools and invoke_api_endpoint alike:
- An operation off
MCP_OAUTH_OPERATIONSanswers403: SSH keys, tunnels, network rules, domains, export and import, registries, members and invitations stay API-key only, since each grants access that outlives the token. New operations are denied until listed, and a spec fails until each v1 operation is classified. - An operation whose route declares a permission the token lacks
(
RequiredOrganizationResourcePermissions, read at startup; an@Allroute such as the exec and file proxies decides every verb on its path) answers403withWWW-Authenticate: Bearer error="insufficient_scope", scope="<granted> <missing>", so a client that re-authorizes keeps the scopes it had.
Sign-in requests every MCP scope, as Supabase's MCP server does: the 401 challenge
names no scope, so clients request the metadata's scopes_supported, which lists all
of MCP_OAUTH_SCOPES. One consent then covers every allowlisted operation; step-up
still covers a client or user that grants fewer. Whether a client completes step-up for the rest is
checked per client on the dev tenant. API-key callers skip both checks; the key's own
permissions apply in REST.
Tools. tools/list returns 13 tools:
| Tools | What they do |
|---|---|
createBox, listBoxes, getBox, startBox, stopBox, removeBox |
The box operations of the same operationId |
startExecution, getExecution |
Run a command; with wait (default) the result carries stdout, stderr, exit_code and complete |
uploadFiles, downloadFiles |
Tar archives as base64 (body_base64), up to 8 MiB; /mcp accepts JSON bodies up to 12 MiB for this, read only after the caller is authenticated, so an unauthenticated call gets its 401 before any of the body is buffered; every other route keeps the default limit |
list_api_endpoints, get_api_endpoint_schema, invoke_api_endpoint |
Find, describe and call any other catalog operation by operationId |
The catalog is built at startup from openapi/box.openapi.yaml,
openapi/admin.openapi.yaml and the product Swagger
document. Tool names and argument schemas are the operations' own; the v1 specs win
an operationId both define. An operation is in the catalog only when this API
registered a route for it, so spec operations only boxlite serve implements
(snapshots, clone, export, import, attachBox) are left out. A product route must
also sit behind CombinedAuthGuard with no runner or proxy guard: runner, proxy,
browser-auth, health and JWT-only routes (organizations, roles, invitations, usage)
are left out.
How a call runs. A tool call never re-implements a route. It is sent to the API's own Express app as the REST request for its operation, so that route's guards, validation, throttles and audit run unchanged:
POST /mcp ─▶ McpOriginGuard, McpAuthGuard ─▶ caller
tools/call ─▶ McpDispatcher ─▶ METHOD /api/... over a per-process Unix socket
no Authorization, x-boxlite-mcp-delegation: <nonce>,
X-Forwarded-For naming the MCP client
─▶ CombinedAuthGuard: mcp-delegation strategy ─▶ same caller ─▶ REST route
The caller's token is never forwarded (MCP forbids token passthrough). The nonce is
32 random bytes, valid for 30 seconds, consumed on first use and held only in the
process's memory; the socket lives in a fresh 0700 temporary directory, so a
request never reaches another cluster worker. A delegated call is cut off, and the
tool answers with an error, when its response passes 8 MiB, when it sends nothing
for five minutes, or when the MCP client disconnects. startExecution with wait
and attachExecution read output from the read-only attach WebSocket over the same
Unix socket (so no TLS name or port is involved), authenticated by a one-time
attach ticket; output printed before the attach is
replayed from the runner's 256 KiB per-stream backlog. A wait that runs out returns
complete: false with the output so far.
Service: apps/runner · Base path: / · Port: 3003 in
deployments (API_PORT environment variable; in-code default 8080)
There is one runner daemon per EC2 instance. GET / and the development-only
Swagger UI are public; every other route requires the runner bearer token.
BoxLite is embedded in the runner, so these calls operate on the local BoxLite
runtime and its microVMs.
Health and introspection · 4 routes
| Method | Path | What it does |
|---|---|---|
GET |
/ |
Reports runner process health. |
GET |
/api/* |
Serves the runner Swagger UI in development only. |
GET |
/metrics |
Exposes Prometheus runner metrics. |
GET |
/info |
Reports runner health, resource metrics, and version. |
Box lifecycle · 8 routes
| Method | Path | What it does |
|---|---|---|
POST |
/boxes |
Creates a box in the embedded BoxLite runtime. |
GET |
/boxes/{boxId} |
Gets local box information. |
POST |
/boxes/{boxId}/destroy |
Destroys a local box. |
POST |
/boxes/{boxId}/start |
Starts a local box. |
POST |
/boxes/{boxId}/stop |
Stops a local box. |
POST |
/boxes/{boxId}/recover |
Recovers a local box. |
POST |
/boxes/{boxId}/is-recoverable |
Checks whether a local box can be recovered. |
POST |
/boxes/{boxId}/network-settings |
Changes a local box's network settings. |
Embedded BoxLite operations · 14 routes
| Method | Path | What it does |
|---|---|---|
POST |
/v1/boxes/{boxId}/exec |
Starts a command inside the local box. |
GET |
/v1/boxes/{boxId}/executions |
Lists live guest execution IDs, including main; stopped boxes return an empty list without booting. |
GET |
/v1/boxes/{boxId}/executions/{execId} |
Gets local execution status. |
DELETE |
/v1/boxes/{boxId}/executions/{execId} |
Kills a local execution. |
WS |
/v1/boxes/{boxId}/executions/{execId}/attach |
Attaches a WebSocket to local execution I/O. |
POST |
/v1/boxes/{boxId}/executions/{execId}/signal |
Sends a signal to a local execution. |
POST |
/v1/boxes/{boxId}/executions/{execId}/resize |
Resizes a local execution's terminal. |
PUT |
/v1/boxes/{boxId}/files?path={path} |
Uploads a tar stream into the local box. |
GET |
/v1/boxes/{boxId}/files?path={path} |
Downloads a local box path as a tar stream. |
GET |
/v1/boxes/{boxId}/metrics |
Returns local box metrics. |
CONNECT |
/v1/boxes/{boxId}/network/tunnel?port={port} |
Opens a raw bidirectional TCP tunnel to a guest port; answers 409 with code box_not_running for a box that is not running, which the proxy uses to wake the box. |
POST |
/v1/boxes/{boxId}/ssh |
Configures guest SSH once after checking that the box is running; otherwise 409 invalid_state. |
POST |
/v1/boxes/{boxId}/ssh/enable |
Enables saved SSH configuration only while running; otherwise 409 invalid_state. |
GET |
/v1/boxes/{boxId}/ssh |
Reads guest SSH status; a stopped box answers disabled status without a handle. |
DELETE |
/v1/boxes/{boxId}/ssh |
Disables guest SSH on a running box; otherwise 409 invalid_state. Never boots. |
The runner Swagger files contain historical routes that are no longer
registered by server.go; this catalog follows runtime registration.
Service: apps/proxy · Base host: proxy.<domain> and
<port>-<box>.proxy.<domain> · Port: 4000 in deployments (PROXY_PORT
environment variable, required)
The proxy is host-routed: a host whose first label parses as <port>-<box>
selects the preview forwarding paths, and any other host serves the base-host
utility routes. The preview-warning acceptance route is handled before host
routing, so it works on every host. How the proxy authenticates these requests
and tunnels them to guest ports is in proxy/README.md.
Proxy routes · 7 routes
| Method | Host and path | What it does |
|---|---|---|
GET |
proxy.<domain>/health |
Reports proxy health and version. |
GET |
proxy.<domain>/login |
Starts OIDC login with a host-only PKCE cookie and signed target state. |
GET |
proxy.<domain>/callback |
Completes the OIDC preview authentication flow. |
GET |
<port>-<box>.proxy.<domain>/__boxlite/auth/callback |
Consumes the login handoff and issues a host-only tunnel session cookie. |
POST |
<any host>/accept-boxlite-preview-warning |
Records preview-warning acceptance and redirects to the redirect form field (the query form is still accepted). |
ANY |
<port>-<box>.proxy.<domain>/{path...} |
Reverse-proxies HTTP and WebSocket traffic to the selected box port after preview access checks. |
CONNECT |
<port>-<box>.proxy.<domain> |
Opens a TCP stream after inbound and tunnel authorization; SDK/CLI sends its organization bearer. |
Service: apps/image-service (binary cmd/registry-proxy) · Base path:
/v2 · Port: 4100 in deployments (REGISTRY_PROXY_PORT environment
variable) · Reached: from inside the stage's network only, at its own Cloud
Run address over HTTP/2
The pull half of the OCI distribution protocol, served so a runner can fetch an
image whose registry credentials only the platform holds. The organization and
the upstream registry ride in the repository name — <org>/<host>/<repo…> —
because one proxy serves every registry and the path is what says which. A
manifest is relayed byte for byte and a blob is streamed, so the digest a client
verifies still covers what the upstream served. How callers are authenticated,
which upstreams are reachable, and how redirects are judged is in
image-service/README.md.
Registry proxy routes · 4 routes
| Method | Path | What it does |
|---|---|---|
GET |
/health |
Reports registry proxy health and version. |
GET HEAD |
/v2/ |
The distribution version check; answers an unauthenticated caller with a Basic challenge. |
GET HEAD |
/v2/{org}/{host}/{repository...}/manifests/{ref} |
Relays the upstream manifest unmodified, with its digest and media type. |
GET HEAD |
/v2/{org}/{host}/{repository...}/blobs/{digest} |
Streams the upstream blob, following a registry's redirect to a permitted address. |
Service: apps/otel-collector · OTLP/HTTP port: 4318 ·
Health ports: 13133 and 13132
These VPC-internal interfaces receive box and host telemetry, resolve each box token to its organization export configuration, and expose collector health. Only the OTLP/HTTP receiver is enabled; OTLP gRPC is not configured.
Collector interfaces · 6 interfaces
| Protocol | Path or service | What it does |
|---|---|---|
POST OTLP/HTTP |
:4318/v1/traces |
Ingests OTLP trace batches. |
POST OTLP/HTTP |
:4318/v1/metrics |
Ingests OTLP metric batches. |
POST OTLP/HTTP |
:4318/v1/logs |
Ingests OTLP log batches. |
GET HTTP |
:13133/health/status |
Reports aggregate and component health. |
GET HTTP |
:13133/health/config |
Returns the health extension's component configuration view. |
| gRPC | :13132 health service |
Serves gRPC health checks. |
Service: apps/infra-local · Unified entry: http://localhost:28080 ·
Ports: fixed host ports, local only
make up in infra-local starts the dependency services as
BoxLite boxes and the application processes on the host. Every listener below is
bound on the developer's machine; the host ports are fixed literals in
services.py and
native.py, not deployment values. From
inside a box the same listeners are reachable as host.boxlite.internal:<port>.
Unified entry point · 10 routes
| Method | Host and path | What it does |
|---|---|---|
ANY |
<digits>-<token>.localhost:28080/{path...} |
Forwards signed port-preview hosts to the preview proxy on 4000. |
ANY |
localhost:28080/pgadmin/* |
Reverse-proxies the pgAdmin UI. |
ANY |
localhost:28080/jaeger/* |
Reverse-proxies the Jaeger UI. |
ANY |
localhost:28080/dex/* |
Reverse-proxies the Dex OIDC provider. |
ANY |
localhost:28080/minio/* |
Reverse-proxies the MinIO S3 API. |
ANY |
localhost:28080/minio-console/* |
Reverse-proxies the MinIO console UI. |
ANY |
localhost:28080/registry/* |
Reverse-proxies the Docker registry v2 API. |
ANY |
localhost:28080/registry-ui/* |
Reverse-proxies the registry browser UI. |
ANY |
localhost:28080/maildev/* |
Reverse-proxies the MailDev caught-mail UI. |
ANY |
localhost:28080/ |
Responds with a plain-text index of the routes above. |
Caddy serves these over plain HTTP with auto_https off; host 28443 is
reserved for a future tls internal block. Its admin API is on host 12019
(container 2019), and GET :12019/config/ is Caddy's own readiness probe —
each of the other services below carries one of its own.
Dependency services · 11 services
| Service | Host port to container port | Interface |
|---|---|---|
postgres |
25432 → 5432 |
PostgreSQL wire protocol; postgresql://boxlite:boxlite@127.0.0.1:25432/boxlite. |
redis |
26379 → 6379 |
Redis protocol, backing caches, the Socket.IO adapter, and proxy lookups. |
minio |
29000 → 9000, 29001 → 9001 |
S3-compatible object storage API and console UI; probed at /minio/health/live. |
minio-init |
none | One-shot minio/mc job that creates the boxlite bucket; no listener. |
registry |
25000 → 5000 |
Docker registry v2 API; probed at /v2/. |
registry-ui |
25052 → 80 |
Registry browser UI. |
dex |
25556 → 5556 |
OIDC provider served under the /dex issuer path. |
jaeger |
26686 → 16686, 26687 → 4317 |
Trace UI, plus an OTLP/gRPC receiver fed by the local collector. |
pgadmin |
25051 → 80 |
Postgres administration UI; probed at /misc/ping. |
maildev |
25053 → 1080, 25054 → 1025 |
Caught-mail UI and the SMTP sink the API sends to; probed at /healthz. |
otel |
24317 → 4317, 24318 → 4318, 23133 → 13133 |
OTLP gRPC and HTTP receivers, plus a health_check extension at /. |
The local collector is the upstream otel/opentelemetry-collector image with an
inline config, not the first-party build described in
Telemetry collector API: it enables OTLP gRPC as
well as HTTP, exports traces to the Jaeger box, and answers health at /
instead of /health/status.
This Dex box serves the inline _DEX_CONFIG from
services.py, written to
/tmp/dex-config.yaml at boot: one public static client, boxlite, whose
redirect URIs cover the dashboard, the Vite dev server, and the CLI loopback
callback at http://127.0.0.1:5555/callback, plus a password database holding
two static users. dex/config.yaml is a separate file for
a separate environment — see
Container-based local environment.
apps/infra deploys no Dex service; deployed stacks point
OIDC_ISSUER_BASE_URL at a hosted provider and reach it through the same
/.well-known/openid-configuration discovery document.
Application processes · 5 processes
| Process | Host port | Interface |
|---|---|---|
api |
3001 |
Control-plane and hosted BoxLite-compatible APIs; probed at /api/health. |
dashboard |
3000 |
Vite dev server for the dashboard app. |
proxy |
4000 |
Preview proxy API (PROXY_PORT). |
registry-proxy |
4100 |
Registry proxy API (REGISTRY_PROXY_PORT); probed at /health. |
runner |
3003 |
Runner API (API_PORT). |
The local API listens on 3001 rather than the deployed 3000, which the
dashboard dev server uses; BOXLITE_LOCAL_API_PORT overrides it. This stack
points the API's SMTP_HOST at its own MailDev box, so the one email the API
sends — the organization invitation — is caught rather than dropped.
Service: apps/scripts · Entry points: yarn dev:dex, yarn e2e:local
A second local environment: local-dex-env.mjs
starts four Docker containers, then runs the same four application processes on
the host through the serve-slim target. It is the only environment here that
runs dex/config.yaml, which it renders to
.boxlite-home/dex/config.local.yaml and mounts read-only into the Dex
container.
Containers · 4 services
| Service | Host port | Interface |
|---|---|---|
dex |
5556 |
OIDC provider: ghcr.io/dexidp/dex:v2.41.1 serving the rendered dex/config.yaml. |
postgres |
5432 |
PostgreSQL wire protocol: postgres:16-alpine, database boxlite. |
redis |
6379 |
Redis protocol: redis:7-alpine. |
registry |
5001 → 5000 |
Docker registry v2 API: registry:2, probed at /v2/boxlite/{name}/manifests/{tag} for images. |
Application processes · 4 processes
| Process | Host port | Interface |
|---|---|---|
api |
3001 |
Control-plane and hosted BoxLite-compatible APIs. |
dashboard |
3000 |
Vite dev server, kept on its own /api proxy path. |
proxy |
4000 |
Preview proxy API. |
runner |
8080 |
Runner API, left on the in-code default rather than 3003. |
These are four of the five projects the boxed stack serves — all but the
registry proxy — on the same 3000, 3001, and 4000 host ports, so the two
local environments cannot run at the same time.
These interfaces are called from this directory but served by systems outside
it, so no route table above owns them. The analytics and billing clients take
their base URL from GET /api/config and disable the corresponding UI when it
is unset; the same document carries PostHog's key and host to the browser.
External interfaces · 4 APIs, 2 SaaS integrations
| Interface | Client | Base URL source | What it covers |
|---|---|---|---|
| Analytics API | libs/analytics-api-client, generated |
analyticsApiUrl |
8 routes: per-box and organization usage, aggregates, and charts, plus box logs, metrics, and traces. |
| Billing API | billingApiClient.ts, hand-written |
billingApiUrl |
22 routes: wallet and top-ups, signup credit delivery, plans, payment methods, invoices, usage series and prices, coupons, billing emails, portal and checkout URLs. |
| Box admission | commerce-box-limit.service.ts |
billingApiUrl |
2 server-side reads on Commerce: GET /plan and GET /organization/{organizationId}/plan, bearer USAGE_EXPORT_TOKEN, used by hosted CREATE BOX. |
| Usage export | api/src/usage/services, hand-written |
usageExport.url |
2 routes on the same Commerce service, server-side: POST /internal/usage-events and POST /internal/allocation-snapshot, bearer USAGE_EXPORT_TOKEN, which must equal Commerce's USAGE_INGEST_TOKEN. |
| PostHog | posthog-js in the dashboard; posthog-node in the API |
posthog.apiKey, posthog.host |
Browser product analytics; server-side api_* operation events from the global metrics interceptor, two groupIdentify calls, and feature-flag evaluation. |
| Pylon | App.tsx widget, production builds only |
pylonAppId |
Outbound support-chat widget; it registers no route here. |
The analytics client is generated from
swagger.json. Its four telemetry
routes duplicate the control plane's own box telemetry
routes, and the dashboard's box telemetry views call the analytics client only —
they disable themselves when analyticsApiUrl is unset rather than falling back.
Nothing in this directory registers the analytics or billing paths.
The Billing API, Box admission, and Usage export rows are the same Commerce
service reached three ways, on URLs that normally differ by path rather than
host. The browser API and Box admission reads live under /api/billing, which
BILLING_API_URL already includes; admission authenticates with the shared
service token. When BILLING_API_URL is configured, the API validates that URL
and the shared token at startup. A request-time transport or 5xx failure is
logged and temporarily leaves creation unlimited without caching the fallback;
rejected credentials and malformed successful responses still return 503.
Successful per-organization resolutions are cached in Redis for 30 seconds. The
internal usage routes are served off the bare origin. The API itself requires
USAGE_EXPORT_URL whenever export is on
and never derives it
(configuration.ts); it is the SST stack
that supplies a default of BILLING_API_URL's origin
(api.ts), so a deployment outside that stack must set
it explicitly. The two stay separate settings because pointing the dashboard at
a billing service and shipping usage to it remain separate decisions. One
caveat on the credential: a USAGE_EXPORT_URL carrying userinfo makes Axios
build Basic auth and drop the bearer header, so the publisher then sends the
URL's credentials rather than USAGE_EXPORT_TOKEN. Commerce reaches back
in one direction only: it resolves organization ownership against this control
plane using the caller's own token. The billing API role and BILLING_API_KEY
that widen organization suspend/unsuspend exist for
Commerce, which does not currently call them.
Admission applies the plan value to the organization's non-finalized Box total.
STOPPED and UNKNOWN Boxes count, while ERROR, DESTROYING, DESTROYED,
ARCHIVING, and ARCHIVED do not. The billing client is hand-written rather
than generated, so neither side detects its divergence. Five of its 20 routes —
the billing-email group — have no
implementation in Commerce, of which only email/verify has a live caller here
(EmailVerify.tsx); Commerce in turn
serves card default/delete, plan/pay-open-charge, and a credit admission API
that nothing in this repository calls.
Non-API services and bundled tools · 7 entries
| Service | Role |
|---|---|
apps/dashboard |
Static browser application; it consumes the control-plane API and the externally served APIs, and registers no server API. |
apps/dex |
Development-only configuration for the third-party Dex OIDC provider, plus a Dockerfile that bakes it into a dex:v2.42.0 image nothing in this repository builds. The endpoints Dex serves are its own, not a BoxLite-owned API; the listener that does run this config is catalogued under Container-based local environment, on dex:v2.41.1. |
apps/infra |
Infrastructure-as-code and deployment tooling; it does not run an application API. |
apps/infra-local |
Local service orchestration; it registers no API of its own, but every listener it starts is catalogued under the local development stack. |
apps/e2e |
End-to-end test client; it calls deployed APIs but does not expose one. |
apps/box-images |
Image build inputs; they produce box images and do not run a service. |
apps/hack, apps/scripts |
Development and code-generation utilities; they register no API of their own, though apps/scripts starts the container-based local environment. |
MinIO, the registry UI, MailDev, Jaeger, pgAdmin and Caddy are bundled only in the local development stack, and a Docker registry appears in both local environments, so all of their interfaces are listed under Local development stack and Container-based local environment rather than here.
Libraries under apps/libs — generated API clients and shared Go packages —
are consumed by the services above; they are not services. A generated client
specification alone is therefore not treated as a live endpoint unless a
runtime application registers the route. Where a client has no in-repo server,
it is listed under Externally served APIs.