Authentication and API keys¶
Every request authenticates with a bearer token in the Authorization
header. There are two kinds of token: short-lived login tokens for people, and
long-lived API keys for scripts and services.
Logging in¶
Exchange an email and password for an access token:
curl -X POST http://localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"demo@fxquinox.dev","password":"demo"}'
# -> {"access_token": "...", "token_type": "bearer"}
Then send it on every request:
With the SDK, pass it to the Session:
API keys¶
For automation, issue an API key tied to a user instead of storing a password.
Keys are returned once and are prefixed fxq_:
curl -X POST http://localhost:8000/auth/api-keys \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"name":"render-farm"}'
# -> {"token": "fxq_...", "name": "render-farm"}
Use an API key exactly like a login token - as the bearer token. The server
recognises the fxq_ prefix and resolves it to its user, so all the same access
rules apply.
The api_key argument is the bearer token sent on every request — a login
access token or an fxq_ API key both work.
Who am I¶
GET /me returns the authenticated user, which is handy for verifying a token.
Authorization model¶
fxquinox is authenticated by default: every API endpoint requires a valid
bearer token (login token or fxq_ API key) and answers 401 Unauthorized
without one. This is enforced centrally in fxq.api.deps and verified by a test
(test_authz.py) that walks the entire route table — a new endpoint added
without auth fails the build.
Public routes¶
A short, deliberate allowlist is reachable without a token:
| Route | Why it's public |
|---|---|
POST /auth/login, POST /auth/register |
Bootstrap a session. |
GET /healthz |
Liveness probe for load balancers / orchestrators. |
GET /media/{key} |
Media is served to <img> tags, which cannot send an Authorization header. Keys are unguessable UUIDs (capability URLs). |
GET /shared/{token} |
Client-review share link — a scoped review of one playlist, gated by an unguessable, revocable token (opt-in per playlist). |
POST /shared/{token}/notes |
Guest feedback on a share link — an anonymous reviewer posts a comment and an approve / needs-changes verdict, scoped to that playlist's versions. |
A share link is not strictly read-only: guests can leave scoped notes and
approvals through POST /shared/{token}/notes, but cannot touch anything outside
the shared playlist.
WebSocket auth
Browsers cannot set an Authorization header on a WebSocket, so the live
event stream (/events/ws) takes the token as a ?token= query parameter
and validates it before accepting the connection.
Roles¶
Tokens carry a role. Checks are enforced server-side — the UI hides privileged actions, but the API is the real boundary:
| Capability | Allowed roles |
|---|---|
| Read entities, query, publish, log own time | any authenticated user |
| Configure the schema (entity types, fields, statuses, steps, workflows, priorities, branding) | Admin |
View another user's tasks / timesheet (/users/{id}/...) |
Admin, Supervisor, Coordinator |
A non-Admin attempting a schema mutation, or a non-privileged user reading
someone else's timesheet, receives 403 Forbidden.
Open registration
POST /auth/register is public so the first user can bootstrap. For a
locked-down deployment, disable or gate it behind your reverse proxy and
provision users via an Admin instead. See the
hardening checklist.
Treat keys as secrets
An API key carries its user's access. Store it in a secret manager or environment variable, never in source control, and revoke keys you no longer need. See the hardening checklist.