Skip to content

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:

Authorization: Bearer <access_token>

With the SDK, pass it to the Session:

from fxqclient import Session
fxq = Session("http://localhost:8000", api_key=access_token)

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.

fxq = Session("http://localhost:8000", api_key="fxq_...")

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.