Authentication API
docsfy supports two ways to authenticate:
- Bearer-token authentication for CLI tools, scripts, and direct API clients
- Session-cookie authentication for the built-in browser UI after a successful login
The same secret shows up under different names depending on the client. In the login API it is sent as api_key, in the web UI it is entered in a password field, and in direct API calls it is sent as a Bearer token.
Actual examples from the codebase:
await api.post<AuthResponse>('/api/auth/login', {
username,
api_key: password,
})
navigate(intendedPath)
self._client = httpx.Client(
base_url=self.server_url,
headers={"Authorization": f"Bearer {self.password}"},
timeout=30.0,
follow_redirects=False,
)
Note: If you are automating docsfy, you usually do not need to call
POST /api/auth/loginfirst. SendAuthorization: Bearer <api-key>on protected requests instead.
How Authentication Works
For protected requests, docsfy checks authentication in this order:
Authorization: Bearer <token>docsfy_sessioncookie
If the Bearer token does not authenticate a user, docsfy falls back to the session cookie.
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if token == settings.admin_key:
is_admin = True
username = "admin"
else:
user = await get_user_by_key(token)
if not user and not is_admin:
session_token = request.cookies.get("docsfy_session")
if session_token:
session = await get_session(session_token)
For database-backed users, cookie-based authentication re-reads the user record on each request. That means deleted users lose access immediately, and role changes take effect on the next request.
The frontend sends the session cookie automatically on same-origin requests:
const config: RequestInit = {
...options,
credentials: 'same-origin',
redirect: 'manual',
headers,
}
A successful login creates an HttpOnly browser cookie with strict same-site behavior and an 8-hour lifetime:
response.set_cookie(
"docsfy_session",
session_token,
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
max_age=SESSION_TTL_SECONDS,
)
Because the cookie is HttpOnly, frontend JavaScript cannot read it directly. The cookie value is also an opaque session token, not the raw API key you used to sign in.
Unauthenticated requests behave differently depending on the path:
/api/*returns401with{"detail": "Unauthorized"}- HTML requests to
/docs/*redirect to/login /api/wsfollows the same model and accepts either?token=<api-key>or thedocsfy_sessioncookie
Tip: Use session cookies for browsers and Bearer tokens for automation. They are different transport mechanisms for the same underlying credentials.
Endpoint Reference
POST /api/auth/login
Creates a browser session from a username/key pair.
Auth required:NoRequest body:JSON object withusernameandapi_keySuccess:200 OKwithusername,role, andis_admin, plus adocsfy_sessioncookie400:Invalid JSON body, or a JSON body that is not an object401:Invalid username or password
Important rules:
- The built-in admin login only works when
usernameis exactlyadminand the submitted secret matchesADMIN_KEY. - Database-backed users must send the username that owns the submitted key.
- The built-in
adminaccount is special and does not come from the users table. - The returned
rolecan beadmin,user, orviewer. is_administruefor the built-in admin and for database-backed users whose role isadmin.
This endpoint is intentionally public and returns JSON. Bad credentials return 401; they do not trigger a redirect.
Note: The web UI labels this value as a password, but the API field name is still
api_key.
POST /api/auth/logout
Clears the current browser session.
Auth required:NoRequest body:NoneSuccess:200 OKwith{ "ok": true }Side effect:If the request includesdocsfy_session, docsfy deletes the stored session and clears the cookie in the response
This endpoint does not revoke API keys or disable Bearer-token access. It only clears the session-cookie flow.
Note:
logoutis safe to call even when the session has already expired or the client is no longer authenticated.
GET /api/auth/me
This is the current-user endpoint. It returns the identity attached to the current request, whether that request was authenticated by a Bearer token or by a session cookie.
Auth required:YesSuccess:200 OKwithusername,role, andis_admin401:No valid Bearer token or session cookie
The response shape is simple:
return JSONResponse(
content={
"username": request.state.username,
"role": request.state.role,
"is_admin": request.state.is_admin,
}
)
Use GET /api/auth/me to:
- confirm that a token or session is still valid
- determine the active role
- decide whether to show admin-only behavior in a client
POST /api/auth/rotate-key
Rotates the current user's own key. In the UI this behaves like a change-password action, but it rotates the same secret used for Bearer-token authentication.
Auth required:YesRequest body:No body or{}generates a new random keyRequest body:{ "new_key": "..." }sets a custom keySuccess:200 OKwithusernameandnew_api_keyResponse header:Cache-Control: no-store400:Malformed JSON, a non-object JSON body, a short custom key, or the built-inadminaccount authenticated viaADMIN_KEY401:No valid authentication
Any authenticated database-backed user can rotate their own key, including viewer, user, and admin roles. The one exception is the built-in admin login that uses ADMIN_KEY.
Key rotation invalidates every existing session for that user:
cursor = await db.execute(
"UPDATE users SET api_key_hash = ? WHERE username = ?",
(key_hash, username),
)
if cursor.rowcount == 0:
msg = f"User '{username}' not found"
raise ValueError(msg)
# Invalidate all existing sessions for this user
await db.execute("DELETE FROM sessions WHERE username = ?", (username,))
await db.commit()
The HTTP response also prevents caching and clears the current browser session:
response = JSONResponse(
content={"username": username, "new_api_key": new_key},
headers={"Cache-Control": "no-store"},
)
response.delete_cookie(
"docsfy_session",
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
)
After a successful rotation:
- the old key stops working immediately
- all existing sessions for that user are deleted
- the current
docsfy_sessioncookie is cleared - you must sign in again with the new key
Warning: Custom keys must be at least 16 characters long.
Warning: The built-in
adminaccount cannot use this endpoint. To change that password, changeADMIN_KEYin the server configuration instead.
Configuration
Authentication depends on two settings:
ADMIN_KEYis required at startup and must be at least 16 characters longsecure_cookiesdefaults toTrue; disable it only for local plain-HTTP development
The server settings are defined like this:
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
admin_key: str = "" # Required — validated at startup
ai_provider: str = "cursor"
ai_model: str = "gpt-5.4-xhigh-fast"
ai_cli_timeout: int = Field(default=60, gt=0)
log_level: str = "INFO"
data_dir: str = "/data"
secure_cookies: bool = True # Set to False for local HTTP dev
When running with Docker Compose, ADMIN_KEY is passed through from the environment:
env_file:
- .env
environment:
- ADMIN_KEY=${ADMIN_KEY}
Warning: If
secure_cookiesstays enabled on plain HTTP local development, the browser will not send the session cookie and login will appear not to stick.Warning:
ADMIN_KEYis more than the built-in admin password. It is also used when hashing stored user keys, so changing it invalidates existing database-backed user keys.
Choosing The Right Flow
- Use Bearer tokens for CLI tools, scripts, and direct API clients.
- Use
POST /api/auth/loginwhen you want a browser session for the built-in UI. - Use
GET /api/auth/meto check who the current request is authenticated as. - Use
POST /api/auth/rotate-keywhen you want to invalidate a user key and issue a new one. - Use
POST /api/auth/logoutonly to clear browser sessions.