Gravity Authentication
Two independent auth systems exist side by side — don't confuse them:
| Admin auth | Customer portal auth | |
|---|---|---|
| Cookie | token |
portal_session / portal_refresh |
| Protects | /admin/* |
/portal/* |
| FastAPI dependency | require_admin (app/deps.py) |
require_portal (app/deps.py) |
| Primary backend | Adhara SDK | Adhara's portal API (app/services/portal_auth.py) |
app/auth.py holds only the shared, framework-free facts both dependencies
need — cookie name constants and the dev-bypass flag checks
(dev_admin_enabled(), dev_portal_enabled(), theme_tweaks_enabled()).
The actual route-guarding logic lives in app/deps.py, as FastAPI
Depends() callables, not decorators — a route declares
admin_ctx: dict = Depends(require_admin) as a parameter, and FastAPI calls
require_admin before the route body runs.
This doc covers admin auth — the one most people mean by "logging into Gravity."
Admin auth: Adhara SDK is the primary path
Real deployments require the Adhara SDK. app/services/adhara.py::validate_token()
calls into it; require_admin (app/deps.py) runs before every /admin/*
route's controller body and checks the token cookie against it:
Browser GET /admin/blog/manage (Cookie: token=...)
→ app/deps.py :: require_admin(request)
→ app/services/adhara.py :: validate_token(token)
- SDK not installed → always returns False
- SDK installed → AdharaClient.auth.validate_token(token)
→ valid: returns {current_user, admin, sdk_available} — becomes the route's admin_ctx argument
→ invalid/missing: raises RedirectRequired('/login')
(a single exception handler in app/main.py turns this into a 302)
Without adhara-sdk installed, validate_token() always returns False
and every /admin/* route redirects to /login, which shows an
SDK-required banner instead of a working login form
(app/controllers/main.py::login() passes sdk_available=False to the
template). app.services.adhara.get_user_info(token) resolves the current
username/admin flag once a request is authenticated.
Client-side, public/js/auth.js (vanilla JS, no jQuery) POSTs to /api and
stores the returned JWT in localStorage and as the token cookie.
Local fallback (GRAVITY_DEV_ADMIN, and UserManagement.py)
Two separate things make local testing possible without a real Adhara account:
GRAVITY_DEV_ADMIN=1— the blunt bypass.require_adminchecksapp.auth.dev_admin_enabled()first and skips the auth check entirely when it's set. This is whatmake editorturns on. Never set this in a deployed environment — none of the four deploy scripts (scripts/deploy_{vercel,gcp,cloudflare,azure}.sh) set it, so a normal deploy stays locked.gravityApp/app/UserManagement.py— a local, file-backed user store (JsonFile.py-based, JWT viaJsonWebToken.py) that still exists and still backs the admin "manage users" UI and theuser_add_new/user_edit/user_deleteJSON commands (see the JSON Command API section of architecture.md). It is a real, working local auth store — not a stub — but it is not what gates/admin/*in a real deployment; Adhara SDK validation is. See architecture.md §3 for the full request-dispatch path.
These two are independent: GRAVITY_DEV_ADMIN=1 bypasses the login check
outright, while UserManagement.py is a genuine (if local-only) credential
store that the JSON command API still uses for user management regardless
of which login path is active.
Portal auth (customer-facing, separate system)
/portal/* uses an entirely different session — require_portal
(app/deps.py), portal_session/portal_refresh cookies, backed by Adhara's
workspace-scoped customer portal API (app/services/portal_auth.py), no
SDK required. It supports silent token refresh before falling back to a
login redirect — an expired portal_session with a still-valid
portal_refresh cookie gets quietly re-minted, with the new cookies set
directly on the dependency's injected Response parameter, so the route body
never needs to know a refresh happened — and its own local dev bypass,
GRAVITY_DEV_PORTAL=1 (see app.auth.dev_portal_enabled() — always a
synthetic local session, even with Adhara configured, since there's no way
to mint a real Adhara-signed token without an actual login). Never set this
in production either.