API keys and credentials
Scoped project keys minted once, stored hashed — plus the other bearer tokens Camplax hands out and where each belongs.
Camplax hands out several kinds of credential, and mixing them up is the fastest way to a 403. The rule of thumb: cx_live_/cx_test_ keys act on one project, cx_cli_ acts as you, cak_live_ only writes telemetry, cxm_ is for AI chat clients.
Which token where
| Prefix | What it is | Where it goes |
|---|---|---|
| (none — cookie) | Console session after sign-in | the browser only — reaches session-only routes like team admin, billing and key management |
cx_cli_… | A CLI token for your user — every project you can reach | CAMPLAX_TOKEN, Authorization: Bearer on CLI/API and MCP calls |
cx_live_… / cx_test_… | Scoped project API keys | Authorization: Bearer on one project's data routes — database, storage, users, logs |
cak_live_… | The ingest key behind CAMPLAX_ANALYTICS_KEY | errors, analytics and flags ingestion — write-only telemetry |
cxm_… / cxr_… | OAuth access/refresh pair minted for an MCP client | Authorization: Bearer on https://camplax.dev/mcp |
If a bearer works at https://camplax.dev but not on your app's own domain, that is expected: cx_cli_/cxm_ identify a user to the platform, while cx_live_/cak_live_ identify a project.
Minting a cx_cli_ token without the CLI
camplax login creates one through the browser device flow. For a machine that cannot open a browser — a CI runner, a hosted agent — mint the same credential in the console under Account → Access tokens → New token, or call POST /v1/cli/tokens with a console session:
curl -X POST https://camplax.dev/v1/cli/tokens \
-H "Cookie: <your console session>" \
-H "Content-Type: application/json" \
-d '{"name": "ci-runner", "expiresInDays": 90, "password": "…"}'
Making one asks you to confirm it is you, like the other dangerous actions: your password (or, with no password on the account, a sign-in in the last ten minutes), plus the six-digit code from your authenticator app when two-factor is on — password and code in the body. The secret is shown once, hashed at rest like a project key. GET /v1/cli/tokens lists each token's name, prefix, created and last-used times; DELETE /v1/cli/tokens/:id revokes. All three routes are session-only on purpose: a leaked cx_cli_ can act as you on the API, but it cannot mint or kill other tokens.
Project API keys
Create them under Project → API keys or through POST /v1/projects/:slug/keys — any owner or member seat, console session only. Creating, rotating and revoking a key each ask you to confirm it is you: your password (or a sign-in in the last ten minutes when the account has none), plus your authenticator code when two-factor is on. A revoked or expired key cannot be rotated back to life — create a new one. A key can do only what its maker's seat can do now: a member's key cannot reach owner-only settings like domains or log drains, an owner who becomes a member keeps a member's reach, and a key stops working when its maker leaves the project or becomes a contributor, billing or viewer. The app token belongs to the app, not a person, so it is never affected. Each key carries a subset of 29 scopes:
| Scope | Unlocks | Scope | Unlocks |
|---|---|---|---|
db:read / db:write | Postgres data API | payments:read / payments:write | store checkout APIs |
storage:read / storage:write | bucket files | ai:invoke | the AI gateway |
users:read | list app users | deploy | trigger deployments |
users:admin | create/disable/reset users | env:read / env:write | secrets vault |
webhooks:send | outbound event webhooks | logs:read | the log stream |
heartbeat:send | report a heartbeat monitor ping | realtime:subscribe / realtime:publish | listen, publish and broadcast on realtime channels |
widgets:read / widgets:write | hosted UI widgets | manage | console-parity routes — mailbox inboxes and sends, analytics, cron, inbound and database webhooks, MCP domain tools |
events:send | send events that resume or start workflows | db-webhooks:ingest | the runtime handing database-webhook row changes over |
auth:captcha | check a sign-in Turnstile token (apps made before November 3, 2026; current apps check their own) | auth:oauth-proxy | redeem a Google/GitHub sign-in through Camplax — preview tokens only |
credits:read | read the prepaid credit balance | flags:read | evaluate feature flags |
email:send | send mail from the project's inboxes | email:receive | claim inboxes whose mail forwards to the app |
forms:read | read form submissions |
The app token Camplax injects as CAMPLAX_PROJECT_TOKEN carries a fixed set in production — ai:invoke, storage, payments, webhooks:send, auth:captcha, heartbeat:send, events:send, realtime:publish, credits:read, email:send/email:receive, forms:read, flags:read, widgets:read/widgets:write and the runtime-only db-webhooks:ingest — so none of it needs a key you create. Deliberately not manage, env:*, deploy or db:*: app code is not trusted with control-plane access. A preview's token is narrower — no payments, email, events, credits or db-webhooks:ingest, auth:oauth-proxy instead — so a preview only ever reaches its own environment. db-webhooks:ingest is accepted only from that production token — never from a key you mint or a camplax dev token.
The console asks for one of three access levels rather than a scope list: Read only (every *:read scope plus realtime:subscribe), Read and write (everything except manage), or Full access (all 29, manage included).
What is actually stored
A key is a cx_live_/cx_test_ prefix plus 32 random characters — about 190 bits — and a six-character checksum of everything before it, minted once. The checksum lets a scanner, and the API itself, throw away a cx_live_…-shaped string that was never minted without a database lookup. The platform stores the key's SHA-256 hash and an 8-char display prefix — never the secret itself. Two consequences:
- The full key is shown exactly once, in the create/roll response. Copy it then; it cannot be recovered.
- A leaked database row cannot be replayed: lookup hashes the presented key and compares digests.
Expiry and lifecycle
| Preset | Behaviour |
|---|---|
0 | No expiry |
30 / 90 / 180 / 365 | Days until the key dies |
Skip expiresInDays (or pass something that is not a number) and the API defaults to 90 days.
- Roll — mints a new key with the same name, scopes, mode and expiry, and revokes the old one in the same call. The new secret is shown once.
- Revoke — flips a flag; the key immediately fails. To restore access you roll a replacement — revoke is not undone.
- Delete — removes the record entirely.
- An expired key authenticates as nothing; requests fail closed.
lastUsedAton each key tells you which ones are dead weight.
Using a key
# db:read scope
curl https://camplax.dev/v1/projects/my-app/database/schema \
-H "Authorization: Bearer cx_live_…"
# any POST needs db:write — even a read-only query — and all SQL passes a safety guard
curl -X POST https://camplax.dev/v1/projects/my-app/database/run \
-H "Authorization: Bearer cx_live_…" \
-H "Content-Type: application/json" \
-d '{"sql":"select count(*) from orders","allowWrites":false}'
A missing scope comes back 403 { "error": "forbidden", "message": "Missing required scope: <scope>. …" } — the message names the scope and the shortest way to get it: a key minted for one environment (like the app token) picks up new scopes on that environment's next deploy, or at once by rotating it under Keys. A revoked, expired or unknown key is a flat 401 — the response never says which, on purpose. A live key of a project Camplax has suspended (for abuse) gets 403 { "error": "project_suspended" } on every route until the project is restored. Keys only authenticate Authorization: Bearer against project routes — they cannot mint other keys, manage the team, or touch deploy hooks.
The other two project secrets
CAMPLAX_ANALYTICS_KEY(cak_live_…) — the injectable SDK key forcx.js, error and analytics ingest. Write-only by design: it can record telemetry, never read your data, so it is safe to expose in client-side config.- Deploy-hook and public tokens — deploy-hook secrets and share tokens are generated per feature; see logs for share URLs and the CLI/API docs for hooks.
Related docs
- REST API — where each credential is accepted.
- Agents and MCP —
cx_cli_bearer versus the OAuthcxm_flow. - Errors — what
cak_live_ingestion looks like.