Appearance
Token scopes
API keys carry an optional list of scopes that limits what they can do. The model is deliberately simple:
- A key with no scopes stored is full access — this is what
apco loginmints, and what you get by omittingscopeswhen creating a token. - A key with scopes can only call routes covered by those scopes.
- Dashboard/bearer sessions are always full access.
- Malformed scope data on a key fails closed: the key gets an empty scope set (no access to scoped routes), never silently full access.
Internally, scopes are flat strings stored in the key's permissions as {"apco": ["deploy", ...]}.
Scope catalog
The exact scope strings (from apps/web/lib/scopes.ts):
| Scope | Label | Grants |
|---|---|---|
projects:read | Read projects | List projects and view project details and status |
projects:write | Manage projects | Create, rename, and delete projects |
deploy | Deploy | Create deployments and roll back to previous deployments |
env:read | Read env vars | Read project environment variables (including values) |
env:write | Write env vars | Set and delete project environment variables |
logs:read | Read logs | Read build and runtime logs |
jobs:read | Read scheduled jobs | View safe definitions, logical history, and attempt metadata |
jobs:write | Manage scheduled jobs | Enable/disable, run immediately, and cancel logical runs |
alerts:read | Read alerts | View observability alert settings and recent incidents |
alerts:write | Manage alerts | Enable alerts, configure webhooks, toggle categories |
metrics:read | Read metrics | Read traffic metrics, resource usage, and analytics |
lifecycle | Start / stop | Start and stop project containers |
database:credentials | Database credentials | Reveal a selected branch's standing URL for local development |
database:query | Database query | Execute SQL as a selected branch's tenant role |
ci:deploy | CI deployment | Setup-only: create deployments for exactly one project and poll/read only deployments created by that same key |
ci:deploy is intentionally absent from the generic combinable-scope picker. The GitHub Actions card creates it as the token's only scope with exactly one project id. The API enforces the same shape again on every use.
Which routes require which scope
| Scope | Routes |
|---|---|
| (any valid non-CI credential) | GET /me (ci:deploy is denied because its contract excludes account identity) |
projects:read | Project/status/deployment reads plus branch status, /database/branches, schema/tables/usage/migrations, and backup list/metadata/download |
projects:write | Project mutation plus branch create/drop, row mutation, backups, and credential rotation; transitionally also satisfies query/credential reveal |
database:credentials | POST /projects/:id/database/dev-credentials?channel= |
database:query | POST /projects/:id/database/query?channel= |
deploy | Deployments, rollback, and code-only promotion |
ci:deploy | POST /projects/:id/deployments, then GET /deployments/:id and GET /deployments/:id/logs only when that deployment was created by the same key |
env:read | GET /projects/:id/env |
env:write | PUT /projects/:id/env |
logs:read | GET /deployments/:id/logs, GET /projects/:id/runtime-logs, GET /deployments/:id/timeline |
jobs:read | GET /projects/:id/jobs, /job-runs, and /job-runs/:runId; job logs also require logs:read |
jobs:write | Definition enable/disable, manual-run, and cancellation routes |
alerts:read | GET /projects/:id/alerts |
alerts:write | PATCH /projects/:id/alerts, PUT/DELETE /projects/:id/alerts/webhook, POST /projects/:id/alerts/webhook/rotate |
metrics:read | GET /projects/:id/metrics, GET /projects/:id/resources, GET /projects/:id/analytics |
lifecycle | POST /projects/:id/stop, POST /projects/:id/start |
| Full access (session or permissionless key) | GET /account, POST /account/set-password, GET /account/usage, GET /github/repos |
| Session only | POST /tokens (API keys get 403 SESSION_REQUIRED) |
| Admin (admin role + full access) | Everything under /admin/* |
Notes:
- No scope covers account-level surfaces.
GET /account/usageand the other/account/*routes require full access; a scoped key gets 403FORBIDDEN_SCOPE. The CLI'sapco dev --syncplan pre-check tolerates this (it fails open and lets the server-side deploy gate decide). - Admin routes require both the
adminrole and full access — a scoped key on an admin account is still rejected (FORBIDDENfor the role check,FORBIDDEN_SCOPEfor the key). - Channel does not grant extra authority: the same project allowlist and scope apply to all three branches. New tokens should use
database:query/database:credentials;projects:writeremains accepted during the transition. Read-only branch routes useprojects:read.
Denial behavior
A scoped key hitting a route it lacks gets:
json
{ "ok": false, "error": { "code": "FORBIDDEN_SCOPE", "message": "This API token is missing the \"deploy\" scope." } }with HTTP 403. Full-access-only routes phrase it as "This endpoint requires a session or a full-access API token." The CLI maps FORBIDDEN_SCOPE to CLOUD_FORBIDDEN_SCOPE — see error codes.
Creating scoped tokens
Dashboard
Tokens page → create token → pick a name, optional expiry, and either "full access" or a scope subset. The plaintext key is shown once.
API (POST /tokens)
Session-only (an API key cannot mint further keys):
bash
curl -X POST https://apco.space/api/v1/tokens \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "ci-deploy", "expiresIn": 2592000, "scopes": ["deploy", "projects:read", "logs:read"] }'- Omit
scopesentirely for a full-access token. An emptyscopesarray is rejected (422) — select at least one scope or omit the field. expiresInis in seconds (optional; enforced to 1–365 days by the auth layer when present).- The response's
token.keyis returned exactly once and never retrievable again.
CLI login
apco login creates a full-access key named cli @ <hostname> and stores it in ~/.config/apco/config.json; apco logout revokes it. For CI, prefer a scoped token in the APCO_TOKEN env var — a deploy pipeline typically needs only deploy, projects:read, and logs:read.
Recommended scope sets
| Use case | Scopes |
|---|---|
| Generic CI that must inspect project status and unrelated build logs | deploy, projects:read, logs:read |
| GitHub Actions generated workflow | setup-only ci:deploy plus one project id (do not combine scopes) |
| Read-only monitoring | projects:read, metrics:read, logs:read |
| Env management bot | env:read, env:write, projects:read |
| Ops runbook automation | projects:read, lifecycle, logs:read |
| Scheduled-job operator | projects:read, jobs:read, jobs:write, logs:read |
| Read-only database inspection | projects:read |
| SQL automation | projects:read, database:query |
| Local Dev credentials | projects:read, database:credentials (plus projects:write to create a missing Dev branch) |
| Full automation / MCP with branch provisioning | full access (omit scopes) |