API
Every route, who may call it, and the conventions the whole surface follows.
Conventions
- There is no global route prefix, and no path segment names an audience. One top-level segment per resource; sub-resources nest under their parent's item path.
- Because the path no longer signals who may call it, a route's protection is readable only from its decorators. The global guard authenticates by default; each handler opts out or gates up explicitly.
- Operator-only capability is a decorator on the handler, never a separate
/adminpath. - Lifecycle verbs are explicit routes (
/start,/stop,/restart), never one:actionsegment — a mistyped sub-resource cannot be swallowed by a catch-all. - Responses are enveloped, and validation errors return a structured field-error map.
Authentication
Two principals resolve to the same AuthenticatedSession shape:
| A session cookie | Issued by sign-in, or by accepting an invitation |
| An API token | st_-prefixed, resolved by the same global guard |
No route has a parallel authorization path.
Documentation route
/docs on the API serves the OpenAPI document — in development and test only.
Routes
Public
| Path | Notes |
|---|---|
GET /health | |
GET /setup-status, POST /setup | /setup refuses with 409 once one user exists |
POST /signup | Gated on SIGNUP_ENABLED. When off, 404 — indistinguishable from a route that was never built |
POST /invitations/accept | The one unauthenticated route that creates a user |
POST /webhooks/:provider | github, gitlab, bitbucket, generic. HMAC-verified, with its own tighter rate limit. The only public route that can start work |
POST /auth/two-factor/verify | Takes the sign-in challenge |
Sign-in and password reset request/confirm under /auth/* are public; the rest of /auth/*
requires a session.
Authentication and users
| Path | Guard |
|---|---|
/auth/* | Mixed, declared per handler. Sign-out, password change, session read, session list, and session revoke require a session |
/auth/two-factor, /auth/two-factor/confirm | Session; enrolling or disabling re-authenticates with the password |
/users, /users/:id/sessions | Session to list; OWNER/ADMIN to list or revoke another member's sessions |
Organizations
| Path | Guard |
|---|---|
GET /organizations/:id, GET /organizations/:id/members, GET /organizations/:id/invitations | Session, organization-scoped |
| Rename, member role change, member removal, invite, cancel invitation | OWNER |
Projects and applications
| Path | Guard |
|---|---|
/projects, /projects/:id/environments | Session, organization-scoped |
/applications, /applications/:id | Session, organization-scoped |
/applications/:id/start, /stop, /restart | Three explicit routes |
/applications/:id/domains, /domains/:domainId/verify | |
/applications/:id/variables | GET returns keys and value lengths only |
/applications/:id/volumes | |
/applications/:id/git-source, /git-source/deploy-key, /git-source/webhook-secret | GET /git-source returns the deploy key's public half and never the private one |
/applications/:id/metrics, /applications/:id/containers |
Deployments
| Path | Notes |
|---|---|
POST /deployments | multipart/form-data — the only non-JSON request body in the API |
GET /deployments, GET /deployments/:id | |
GET /deployments/:id/logs | The archived log |
GET /deployments/:id/live | Server-Sent Events, carrying the session cookie |
POST /deployments/:id/rollback |
Servers
| Path | Notes |
|---|---|
GET /servers, GET /servers/:id | |
GET /servers/:id/sample, /metrics, /containers | |
GET /servers/:id/live | Server-Sent Events |
POST /servers, POST /servers/:id/agent-token | The issued agent token appears in these two responses and nothing else |
DELETE /servers/:id | Refuses while anything is deployed to the server |
Ingress, Git credentials, services
| Path | Notes |
|---|---|
GET /proxy | Read-only ingress and certificate state for one server, named by an optional serverId query |
/git-credentials, /git-credentials/:id/repositories | GitHub App installations and the repository picker |
/services, /services/:id | |
/services/:id/start, /stop, /restart | |
/services/:id/connection | The only route that discloses generated credentials. Writes an audit row |
/services/:id/backups |
Alerts, tokens, billing, audit
| Path | Notes |
|---|---|
/alerts, /alerts/summary, /alerts/:id/resolve | /summary is three counts for the shell badge |
/notification-channels, /notification-channels/:id | POST returns a webhook signing secret once |
/api-tokens, /api-tokens/:id | POST returns the issued token once. DELETE revokes rather than deletes |
/plans, /subscription | Session. PUT /subscription is operator-gated by decorator |
/audit-logs | Session and an operator role |
Why PUT /subscription is operator-gated
With the manual billing provider, a plan change is a grant. A member who could grant themselves the largest plan would make every limit decorative.
Not an HTTP route
/agent/connect is a WebSocket upgrade, authenticated during the handshake by the
x-agent-token header and never by a user session. A rejected agent gets a 401 and no
socket. It is a transport channel rather than a REST resource — the one documented exception
to resource naming.
Origins and cookies
A self-hosted deployment serves the dashboard and the API on one origin, so session cookies
stay same-origin and CORS_ALLOWED_ORIGINS can stay empty. Split-origin (cloud) deployments
list the dashboard's origin there, and in AUTH_TRUSTED_ORIGINS for CSRF.