Skip to content

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 login mints, and what you get by omitting scopes when 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):

ScopeLabelGrants
projects:readRead projectsList projects and view project details and status
projects:writeManage projectsCreate, rename, and delete projects
deployDeployCreate deployments and roll back to previous deployments
env:readRead env varsRead project environment variables (including values)
env:writeWrite env varsSet and delete project environment variables
logs:readRead logsRead build and runtime logs
jobs:readRead scheduled jobsView safe definitions, logical history, and attempt metadata
jobs:writeManage scheduled jobsEnable/disable, run immediately, and cancel logical runs
alerts:readRead alertsView observability alert settings and recent incidents
alerts:writeManage alertsEnable alerts, configure webhooks, toggle categories
metrics:readRead metricsRead traffic metrics, resource usage, and analytics
lifecycleStart / stopStart and stop project containers
database:credentialsDatabase credentialsReveal a selected branch's standing URL for local development
database:queryDatabase queryExecute SQL as a selected branch's tenant role
ci:deployCI deploymentSetup-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

ScopeRoutes
(any valid non-CI credential)GET /me (ci:deploy is denied because its contract excludes account identity)
projects:readProject/status/deployment reads plus branch status, /database/branches, schema/tables/usage/migrations, and backup list/metadata/download
projects:writeProject mutation plus branch create/drop, row mutation, backups, and credential rotation; transitionally also satisfies query/credential reveal
database:credentialsPOST /projects/:id/database/dev-credentials?channel=
database:queryPOST /projects/:id/database/query?channel=
deployDeployments, rollback, and code-only promotion
ci:deployPOST /projects/:id/deployments, then GET /deployments/:id and GET /deployments/:id/logs only when that deployment was created by the same key
env:readGET /projects/:id/env
env:writePUT /projects/:id/env
logs:readGET /deployments/:id/logs, GET /projects/:id/runtime-logs, GET /deployments/:id/timeline
jobs:readGET /projects/:id/jobs, /job-runs, and /job-runs/:runId; job logs also require logs:read
jobs:writeDefinition enable/disable, manual-run, and cancellation routes
alerts:readGET /projects/:id/alerts
alerts:writePATCH /projects/:id/alerts, PUT/DELETE /projects/:id/alerts/webhook, POST /projects/:id/alerts/webhook/rotate
metrics:readGET /projects/:id/metrics, GET /projects/:id/resources, GET /projects/:id/analytics
lifecyclePOST /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 onlyPOST /tokens (API keys get 403 SESSION_REQUIRED)
Admin (admin role + full access)Everything under /admin/*

Notes:

  • No scope covers account-level surfaces. GET /account/usage and the other /account/* routes require full access; a scoped key gets 403 FORBIDDEN_SCOPE. The CLI's apco dev --sync plan pre-check tolerates this (it fails open and lets the server-side deploy gate decide).
  • Admin routes require both the admin role and full access — a scoped key on an admin account is still rejected (FORBIDDEN for the role check, FORBIDDEN_SCOPE for 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:write remains accepted during the transition. Read-only branch routes use projects: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 scopes entirely for a full-access token. An empty scopes array is rejected (422) — select at least one scope or omit the field.
  • expiresIn is in seconds (optional; enforced to 1–365 days by the auth layer when present).
  • The response's token.key is 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.

Use caseScopes
Generic CI that must inspect project status and unrelated build logsdeploy, projects:read, logs:read
GitHub Actions generated workflowsetup-only ci:deploy plus one project id (do not combine scopes)
Read-only monitoringprojects:read, metrics:read, logs:read
Env management botenv:read, env:write, projects:read
Ops runbook automationprojects:read, lifecycle, logs:read
Scheduled-job operatorprojects:read, jobs:read, jobs:write, logs:read
Read-only database inspectionprojects:read
SQL automationprojects:read, database:query
Local Dev credentialsprojects:read, database:credentials (plus projects:write to create a missing Dev branch)
Full automation / MCP with branch provisioningfull access (omit scopes)

APCO Cloud — ship apps with one command.