Admin Panel Configuration¶
The admin panel has a deliberately small surface area: it's a SQLAdmin instance gated by a username/password from environment variables. Configuration boils down to a handful of .env values.
Environment Variables¶
# Toggle the admin panel (default: true)
ADMIN_ENABLED=true
# Admin login credentials
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-secure-password
# Used for admin session encryption (same SECRET_KEY as the rest of the app)
SECRET_KEY=<openssl rand -hex 32>
That's the whole admin-specific config. Everything else (engine, models, mount path) is hardcoded in src/interfaces/admin/initialize.py for simplicity.
Backing Settings Classes¶
The variables map to two settings classes in src/infrastructure/config/settings.py:
AdminSettings—ADMIN_NAME,ADMIN_EMAIL,ADMIN_USERNAME,ADMIN_PASSWORD,DEFAULT_TIER_NAME. Used by both the admin panel login andscripts/setup_initial_data.pyto bootstrap the first superuser.SQLAdminSettings—ADMIN_ENABLED. Single toggle for the admin panel.
What Happens at Startup¶
infrastructure/main.pycallscreate_admin_interface(app)frominterfaces/admin/initialize.py- If
ADMIN_ENABLED=false, the function returnsNoneand the admin panel is not mounted - Otherwise, an
AdminAuthbackend is constructed usingSECRET_KEY - A SQLAdmin
Admininstance is created against the app's existing databaseengine register_admin_views(admin)addsUserAdminandTierAdmin(fromviews/)- The admin app is mounted at
/admin
Login Authentication¶
Login flow (in interfaces/admin/auth.py):
class AdminAuth(AuthenticationBackend):
async def login(self, request: Request) -> bool:
form = await request.form()
username = form.get("username")
password = form.get("password")
settings = get_settings()
if username == settings.ADMIN_USERNAME and password == settings.ADMIN_PASSWORD:
request.session.update({"admin_authenticated": True})
return True
return False
Notes:
- Credentials come from environment variables, not the database. Restart the app to change them.
- Only one admin login is supported. There's no multi-admin user table.
- The session is encrypted with
SECRET_KEYvia Starlette'sSessionMiddleware. - Logout clears the session:
request.session.clear().
If you need multiple admin operators, see User Management for ways to extend this.
Mount Path¶
The admin panel is hardcoded at /admin (defined when Admin(...) is instantiated). To change the path, edit src/interfaces/admin/initialize.py:
admin = Admin(
app=app,
engine=engine,
authentication_backend=authentication_backend,
title="Admin",
base_url="/management", # add this to change the mount path
)
If you change it, also update any internal links in your frontend or operational docs.
Database Connection¶
SQLAdmin reuses the same SQLAlchemy engine the rest of the app uses (imported from infrastructure/database/session.py). There's no separate admin database connection or pool to configure.
Session Cookies¶
The admin login uses Starlette's SessionMiddleware, which is added to the FastAPI app in src/interfaces/main.py:
Cookie behavior:
- HTTP-only by default
- Encrypted/signed with
SECRET_KEY - Same-site
lax - Not marked
Secureautomatically — if you serve the app over HTTPS, setSESSION_SECURE_COOKIES=trueand adjust the middleware as needed (the StarletteSessionMiddlewaredoesn't have a built-in production-secure flag the way our session backend does)
For production behind HTTPS, you'll typically want to:
- Terminate TLS at the proxy / load balancer
- Strip
/adminfrom public-facing routing entirely (see Production Hardening below)
Development vs Production¶
Development¶
The default .env.example is already development-ready:
ENVIRONMENT=development
ADMIN_ENABLED=true
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-secure-password
SECRET_KEY=insecure-secret-key-change-this-in-production
Open http://localhost:8000/admin, log in, and you have access to Users and Tiers.
Production Hardening¶
Three options, ordered by aggressiveness:
-
Disable entirely
Simplest. The admin panel never mounts. Run admin tasks via scripts (uv run python -m scripts.setup_initial_data, custom one-offs) or temporary overrides. -
Restrict at the proxy/load balancer Keep
ADMIN_ENABLED=truebut only allow the/adminpath from your VPN's CIDR range or a specific IP allowlist. The app stays the same; the network blocks public access. -
Use a strong unique password If you can't restrict at the network layer, treat
ADMIN_PASSWORDlike a production secret:- Pull from a secrets manager at deploy time, never commit
- Rotate periodically
- Use a long, high-entropy password (the production security validator will refuse to start the app if
SECRET_KEYis the placeholder, but it doesn't validateADMIN_PASSWORD)
The Production Security Validator (infrastructure/security/) checks several things at startup when ENVIRONMENT=production, but admin credentials aren't currently in the validation list. Be deliberate about what you set.
Environment Detection¶
The admin panel itself doesn't change behavior between local / development / staging / production — it's the same SQLAdmin app. What changes is the surrounding environment:
- Cookie security: derived from your reverse proxy / TLS setup, not from the
ENVIRONMENTsetting - Logging: admin actions go through the same logger configured by
infrastructure/logging/ - Session backend: Starlette's
SessionMiddlewareis in-memory + cookie-based, not the same as the API'sSESSION_BACKEND(Redis/memcached/memory). Restart-resilience for the admin login isn't relevant — admins re-log-in fine.
Troubleshooting¶
/admin returns 404¶
Check ADMIN_ENABLED. If it's false (or unset and Pydantic resolves to a falsy value), the admin app isn't mounted. Verify with:
cd backend
uv run python -c "from src.infrastructure.config.settings import get_settings; print(get_settings().ADMIN_ENABLED)"
Login form keeps rejecting credentials¶
- Confirm
ADMIN_USERNAMEandADMIN_PASSWORDinbackend/.envmatch what you're typing - Restart the app after changing env vars (settings are read at startup)
- If running in Docker, confirm the env vars are actually reaching the container (
docker compose exec app env | grep ADMIN_)
Admin session keeps logging out¶
The Starlette SessionMiddleware cookie's lifetime is controlled by the browser (it's a session cookie). For longer-lived admin sessions, edit the middleware setup in src/interfaces/main.py to pass max_age=...:
app.add_middleware(
SessionMiddleware,
secret_key=settings.SECRET_KEY,
max_age=60 * 60 * 8, # 8 hours
)
Wrong engine connection / "no such table"¶
The admin uses the same engine as the API, which means it requires CREATE_TABLES_ON_STARTUP=true (default) or applied Alembic migrations. If /admin shows views but they're empty / error, check:
Next Steps¶
- Adding Models — Register your own models with the admin
- User Management — Extending admin authentication
- Production — Production hardening checklist