Authentication
Every authenticated request carries the same header:
Authorization: Bearer <credential>The API accepts two kinds of credential on it and tells them apart by shape.
| Credential | Who uses it | Looks like |
|---|---|---|
| Firebase ID token | the dashboard, in a browser | a JWT (three base64 segments) |
| CAVS API token | CLI, SDKs, CI, scripts, git-lfs |
starts with cavs_ |
Human sign-in (Firebase)
Section titled “Human sign-in (Firebase)”Identity for people is Firebase Authentication. The dashboard signs the user in, obtains an ID token, and sends it on every API call. The API verifies the RS256 signature against Google’s public JWKS and checks the issuer and audience against its configured Firebase project.
On first sign-in the API creates the corresponding user record; subsequent sign-ins are a single indexed read.
GET /api/v1/users/me HTTP/1.1Host: cavsnode.comAuthorization: Bearer eyJhbGciOiJSUzI1NiIsIm…curl -sS -X POST https://cavsnode.com/api/v1/auth/session \ -H "Authorization: Bearer $FIREBASE_ID_TOKEN"Machine authentication (API tokens)
Section titled “Machine authentication (API tokens)”Tokens are created per organization in the dashboard (Settings → Tokens) or via
POST /organizations/{org}/tokens. Three families, distinguished by prefix:
| Prefix | Kind | Acts as | Typical use |
|---|---|---|---|
cavs_pat_ |
Personal access token | you | your laptop, the CLI |
cavs_repo_ |
Repository token | itself, bound to one repository | one deploy target |
cavs_ci_ |
CI/machine token | itself | pipelines |
Only the SHA-256 hash is stored. The secret is displayed once at creation and cannot be recovered. Every successful use updates the token’s last-seen timestamp and IP, which you can review in the token list.
curl -sS https://cavsnode.com/api/v1/users/me \ -H "Authorization: Bearer cavs_pat_XXXXXXXXXXXXXXXX"Full lifecycle, scope selection and rotation: API tokens.
How permission is decided
Section titled “How permission is decided”Authentication establishes who. Authorization then composes five inputs, and any one of them can deny:
organization role from your membership + repository override per-repository access level, if any + token scope a hard cap — never widens anything + account status must be ACTIVE + organization status must be activeFor a personal access token the effective permission is
your role ∩ the token's scope. Losing a role immediately narrows every PAT you
hold. For repo and CI tokens there is no user, so the scope alone decides.
See Roles & permissions for the matrix.
Where credentials come from
Section titled “Where credentials come from”cav login validates the token against the API, then writes it to:
~/.config/cav/config.tomlapi_base = "https://cavsnode.com"token = "cavs_pat_…"account = "[email protected]"Prefer the interactive prompt or --token-stdin so the secret stays out of your
shell history:
cav login # promptsecho "$CAVS_TOKEN" | cav login --token-stdinPrecedence for the API base URL: --api flag → $CAVS_API → the stored config
→ the built-in default.
Resolution order in every SDK: explicit argument → $CAVS_TOKEN → the SDK’s
config file, if it has one.
export CAVS_TOKEN=cavs_ci_...export CAVS_API=https://cavsnode.comfrom cavs import CAVSclient = CAVS.from_env()# orclient = CAVS(token="cavs_ci_...", api="https://cavsnode.com", org="acme-ai")import { CAVS } from '@cavsnode/cavs-sdk';const client = CAVS.fromEnv();SDKs redact the token from repr(), toString(), log output and exception
messages, and never log full request headers or presigned URLs.
Git LFS needs a credential for lfs.url. Either the credential helper:
git config credential.helper store# On the first transfer, any username + your CAVS token as the password.Or an explicit header, scoped to the CAVS origin so it never leaks to your Git host:
git config --add http.https://cavsnode.com/.extraHeader \ "Authorization: Bearer $CAVS_TOKEN"Store the token in your platform’s secret store and expose it as an environment variable. Never commit it, never echo it, and never interpolate it into a URL.
env: CAVS_TOKEN: ${{ secrets.CAVS_TOKEN }} CAVS_API: https://cavsnode.comvariables: CAVS_API: https://cavsnode.com# CAVS_TOKEN as a masked, protected CI/CD variableUse a cavs_ci_ token with the narrowest scope the job needs, and one token per
pipeline so you can revoke precisely. See CI/CD recipes.
Presigned URLs are credentials too
Section titled “Presigned URLs are credentials too”The data plane hands you short-lived signed object-storage URLs. Anyone holding one can perform that single operation until it expires (15 minutes by default).
- Never log them, never put them in error reports or CI output.
- Don’t persist them; re-request instead — authorization is cheap.
- They carry no identity, so they don’t appear in the audit log. The authorization call that produced them does.
Statuses that deny
Section titled “Statuses that deny”| Condition | Response |
|---|---|
No Authorization header |
401 unauthorized — “missing bearer token” |
| Malformed or expired Firebase token | 401 unauthorized — “invalid token” |
| Unknown, expired or revoked API token | 401 unauthorized — “invalid or expired token” |
User status is not ACTIVE |
403 forbidden — “account is suspended” |
| Role, override or scope insufficient | 403 forbidden |
Public endpoints
Section titled “Public endpoints”Three routes need no credential:
| Endpoint | Purpose |
|---|---|
GET /healthz |
Liveness. Internal only — an external request hits the load balancer’s own 404. |
GET /readyz |
Readiness, including a database ping. Use this for external checks. |
GET /api/v1/plans |
The public plan catalogue. |
POST /api/v1/billing/webhook |
The billing provider’s callback, verified by signature rather than a bearer token. |
- API tokens — create, scope, rotate, revoke.
- Roles & permissions — the full matrix.
- Security model — encryption, isolation, what we store.

