Skip to content

Latest commit

 

History

History
1056 lines (872 loc) · 82.3 KB

API.md

File metadata and controls

1056 lines (872 loc) · 82.3 KB

BoxLite application API catalog

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:

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.

Services at a glance

API Service Base path or host Deployed port
Control-plane API apps/api /api 3000
Hosted BoxLite-compatible API apps/api /api/v1 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.

Admin organization metadata

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.

Control-plane API

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 · 11 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.
GET /api/regions Lists regions available to the organization.
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.
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 · 3 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.
DELETE /api/images/registries/{id} Removes a login, unless a box still pulls through it (409 lists the boxes).
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.
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/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.

Hosted BoxLite-compatible API

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 · 15 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/{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.
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.
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 without resuming the box or counting as activity.

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.

Runner CPU incidents

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. Returns only the first N matching rows in ID ascending order.
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"
  }]
}

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. Results are ordered by ID ascending and capped at limit; additional matching 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 matches, so this endpoint does not guarantee full incident enumeration. 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.

Runner API

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 · 13 routes
Method Path What it does
POST /v1/boxes/{boxId}/exec Starts a command inside the local box.
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.
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; never boots the box.

The runner Swagger files contain historical routes that are no longer registered by server.go; this catalog follows runtime registration.

Preview proxy API

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.

Registry proxy API

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.

Telemetry collector API

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.

Local development stack

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.

Container-based local environment

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.

Externally served APIs

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.

Services without a first-party API

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.