Admin Panel
The one authenticated surface — signing in, what each screen is for, and what gets audited.
Everything in the public API is read-only. Editing the catalog happens at /admin, behind a login.
Signing in
One account, configured through the environment:
| Setting | What |
|---|---|
ADMIN_USERNAME |
the username |
ADMIN_PASSWORD_HASH_FILE |
path to a file holding an argon2id hash, never the password |
ADMIN_SECRET_KEY_FILE |
path to a file holding the session-cookie signing key |
(ADMIN_PASSWORD_HASH and ADMIN_SECRET_KEY also work as direct values, and are
what you would use locally. In production the *_FILE form is required — see the
warning below.)
Generate the hash on the machine that will hold it:
python -m app.cli hash-password
# Password: ········
# $argon2id$v=19$m=65536,t=3,p=4$...
Never put the hash in a compose
.env
docker compose expands $ in the same env_file it uses for its own variable
substitution. An argon2 hash is full of $, so $argon2id$v=19$m=65536,…
written into .env reaches the container as =19=65536,…. The panel starts,
reports itself configured, and rejects the correct password with 401 — with no
error anywhere to explain why. Mount both values as files instead; the
production compose file already does.
The plaintext password is never written anywhere — not to .env, not to docker inspect, not to a deploy log. A leaked environment yields a hash to crack rather than a working credential.
Production refuses to start without these
With APP_ENV=production and the admin enabled, a missing ADMIN_SECRET_KEY or ADMIN_PASSWORD_HASH is a startup error. An empty signing key would make every session cookie forgeable, and failing loudly at boot beats serving that.
Sessions are signed cookies, HttpOnly, SameSite=Lax, Secure in production, valid 12 hours. Failed logins are throttled per client IP (10 attempts / 5 minutes). Changing ADMIN_USERNAME invalidates every existing session.
The screens
Overview — catalog counts, and a coverage table per provider. The columns that matter are explicit (pinned), derived (answered by the fallback ladder) and unsupported, counted over active regional languages only, because base languages need no mapping. The derived count is your work queue.
Providers — the same table, plus per-provider settings: name, category, fallback policy, aliases, and a copy mappings tool for onboarding.
Matrix — the main screen. See Editing the Matrix.
Languages — search across names, codes and aliases; edit a language; add or remove aliases; create a new one. Each language page also shows what every provider receives for it, editable a row at a time.
Tools — a live resolve tester running the exact code path /v1/resolve uses, and a JSON export of the whole catalog in the same shape the seed files use.
Audit — every write, append-only, newest first.
What is audited
Every mutation records the actor, the action, the entity, its key, and a before/after snapshot. The actor is the admin username plus the client IP — the panel has one account, so the IP is what distinguishes two people using it.
Audit rows are never edited or deleted by the application.
If the panel is unreachable
| Symptom | Cause |
|---|---|
503 admin_unavailable |
ADMIN_PASSWORD_HASH or ADMIN_SECRET_KEY is empty |
| "Session expired" on every submit | cookies blocked, or ADMIN_COOKIE_SECURE=true while serving over plain HTTP |
429 |
login throttle — wait out the 5-minute window |
Setting ADMIN_ENABLED=false removes the panel entirely; the public API is unaffected.
Next
- Editing the Matrix — the screen you will spend your time in
- Pin a provider code