API authentication & RBAC
GET /healthz is public. Everything under /v1 requires an
Authorization: Bearer <token> header. There are three token kinds, all resolved
by the same middleware (src/auth/middleware.rs).
Token kinds
| Token | Origin | Scope |
|---|---|---|
| Bootstrap admin token | ~/.config/denia/admin.token (generated by denia setup) | Super-admin across all projects. HMAC-SHA256 verified in constant time; never stored in the DB. |
| Session token | POST /v1/auth/login | A user's session (TTL in SQLite). Hashed before storage. |
| API token | POST /v1/api-tokens | Long-lived named token for CLI/automation (denia auth). Hashed before storage. |
Bootstrap → account
The admin token is exchanged once for a real account:
POST /v1/bootstrap
{ "username": "admin", "password": "<strong-password>" }
Thereafter, users log in (POST /v1/auth/login) for a session token, and mint API
tokens (POST /v1/api-tokens) for the CLI. Inspect the current principal with
GET /v1/me.
Project-scoped roles
Every user has a membership role per project, ordered Viewer (0) < Operator (1) < Admin (2). Each route enforces a minimum role for the target project; the super-admin bypasses membership checks. See Projects & RBAC, ADR-008.
| Capability | Minimum role |
|---|---|
| Read services/deployments/jobs/logs/metrics (env redacted) | Viewer |
| Create/update services & jobs, deploy, manage domains/registries, open console, trigger runs | Operator |
| Manage project members and roles | Admin |
| Create/delete projects, manage users, registry GC | Super-admin |
When a caller lacks the role, the API returns 403 Forbidden, or a 404-style
message where revealing existence would itself leak information.
WebSocket exception
The service console websocket
(GET /v1/services/{id}/console/ws) cannot carry an Authorization header from a
browser, so it authenticates with a single-use 30s ticket minted over normal
bearer-authenticated HTTP. Bearer tokens never appear in a websocket URL.