Multi-Key Authentication

Multiple named API keys per account with per-key audit trail, last-key lockout protection, and soft-delete revocation.

The model

Every CueAPI account holds a collection of API keys, not a single one. Each key is a row in api_keys with:

  • a globally unique hash used for authentication
  • a short prefix (e.g. cue_sk_a3f9) shown in the dashboard for identification
  • a user-provided label (e.g. claude-desktop, ci-server, github-actions)
  • a creation source (user / register / migration / mcp-exchange)
  • a last-used timestamp and an optional revocation timestamp

Auth resolves a bearer token against api_keys.key_hash. Revoked keys return 401 key_revoked — a distinct error code from invalid_api_key so clients can surface the right message.

Why multiple keys

Rotation without downtime. Mint the new key, update every consumer, revoke the old key. No window where nothing authenticates.

Per-agent credentials. Each MCP host, CI pipeline, worker daemon, or script gets its own named key. When one leaks, you revoke exactly that key — nothing else stops working.

Audit granularity. Every key's last_used_at and creation source are tracked in api_key_audit_log. "Which key did this request come from" is answerable, not just "which user."

Claude Projects isolation. For the remote MCP flow (Stage C of CueAPI's roadmap), each Claude.ai Custom Connector authorization produces its own key labeled mcp:{client}:{connection-id}. Different Projects get different keys, so their cues and executions stay partitioned.

Lifecycle

Creation

Two sources:

  1. Registration. POST /v1/auth/register creates a user row AND a default api_keys row with label="default", created_by="register". The plaintext is returned in the response once.

  2. Explicit. POST /v1/auth/keys with a label mints a new key (rate-limited to 10/user/hour). Also returns plaintext once.

  3. MCP exchange. POST /v1/auth/mcp-exchange (see MCP docs) mints a key for a remote MCP OAuth flow. Labeled mcp:{client_id}:{label} automatically.

Revocation

DELETE /v1/auth/keys/{id} with X-Confirm-Destructive: true flips revoked_at to now. The key stops authenticating immediately (Redis auth cache is invalidated synchronously).

Last-key protection: the API refuses to revoke the user's last non-revoked key with 409 last_key_protected. Users can't lock themselves out by accident.

Soft-delete grace

Revoked rows stay in the database for 30 days so:

  • GET /v1/auth/keys still lists them with their revoked_at timestamp
  • api_key_audit_log rows referring to them keep their target
  • A surprise "we need the audit trail back" request has a 30-day window to recover

After 30 days, a cleanup job hard-deletes the row and writes a final api_key_audit_log entry with event_type="hard_deleted".

Legacy regenerate

POST /v1/auth/key/regenerate still works for single-key accounts — it rotates the one non-revoked key in place (updates both the api_keys row and the legacy users.api_key_hash column for backward compatibility). When the account has more than one key, regenerate returns 409 multiple_keys asking you to use POST /v1/auth/keys + DELETE instead (the rotate-without-downtime pattern).

Audit trail

Every lifecycle event writes a row to api_key_audit_log:

event_typeWhen
createdKey minted (register / POST keys / mcp-exchange)
renamedLabel changed via PATCH
regeneratedSingle-key account used legacy /key/regenerate
revokedSoft-delete via DELETE
consumed_by_mcp_exchangeMinted specifically for a remote MCP OAuth flow
hard_deletedCleanup job removed the row after 30-day grace

Each row captures IP, user-agent, and a JSONB metadata blob. The log is append-only — no update, no delete — and survives the key row itself (via ON DELETE SET NULL on api_key_id).

Backward compatibility

The users.api_key_hash column still exists and still holds a valid hash for every pre-multi-key user. Auth reads from api_keys first; if no row matches, it falls back to the legacy column. That means:

  • Existing CLI installations and workers continue authenticating without any config change
  • An emergency rollback (revert auth.py to the old query) works — the legacy column still has the same value for every user who pre-dates multi-key scoping
  • Tests that create users directly (bypassing registration) still auth, so test suites don't break

Full detail of the backward-compat contract lives in migration 039 on cueapi-core.

How do I know if my agent ran successfully?
Ctrl+K