Authentication and Roles
docsfy uses one authentication model across the web UI, API, and CLI:
- The built-in bootstrap admin signs in as
adminwith theADMIN_KEYenvironment variable. - Everyone else is a database-backed user with a role of
viewer,user, oradmin. - The browser exchanges a username + API key for a session cookie.
- API and CLI clients can send the same API key directly as a Bearer token.
Tip: In the web UI, the field is labeled Password, but the backend and CLI call the same secret an API key. They are the same credential.
Two kinds of admin access
There are two ways to have admin privileges in docsfy:
- The built-in
adminaccount, configured fromADMIN_KEY - A database-backed user whose role is
admin
They can both use the admin panel and admin API. The main difference is credential management: database-backed admins can rotate their own keys through the app, while the built-in admin account is rotated by changing ADMIN_KEY.
Built-in admin login
The built-in admin account is how you bootstrap a new deployment. It is not created from the admin panel and it is not looked up in the users table. Instead, the server reads ADMIN_KEY from the environment at startup, and login grants admin access only when the username is exactly admin.
From .env.example:
# Required: Admin password (minimum 16 characters)
ADMIN_KEY=
# Data directory for database and generated docs
DATA_DIR=/data
# Cookie security (set to false for local HTTP development)
SECURE_COOKIES=true
From src/docsfy/api/auth.py:
# Check admin -- username must be "admin" and key must match
if username == "admin" and hmac.compare_digest(api_key, settings.admin_key):
is_admin = True
authenticated = True
role = "admin"
else:
# Check user key -- verify username matches the key's owner
user = await get_user_by_key(api_key)
if user and user["username"] == username:
authenticated = True
role = str(user.get("role", "user"))
if role == "admin":
is_admin = True
The server also validates ADMIN_KEY on startup. If it is missing or shorter than 16 characters, docsfy exits instead of starting with a broken auth setup.
Warning: The username
adminis reserved. You cannot create a database-backed user namedadmin,Admin, orADMIN.
Database-backed users
Admins can create additional users from the admin panel or the CLI. These users are stored in the SQLite database under DATA_DIR, with a role of viewer, user, or admin.
Raw API keys are not stored in the database. Instead, docsfy stores a keyed HMAC hash of the API key, and auto-generated keys use a docsfy_ prefix.
From src/docsfy/storage.py:
def hash_api_key(key: str, hmac_secret: str = "") -> str:
"""Hash an API key with HMAC-SHA256 for storage.
Uses ADMIN_KEY as the HMAC secret so that even if the source is read,
keys cannot be cracked without the environment secret.
"""
# NOTE: ADMIN_KEY is used as the HMAC secret. Rotating ADMIN_KEY will
# invalidate all existing api_key_hash values, requiring all users to
# regenerate their API keys.
secret = hmac_secret or os.getenv("ADMIN_KEY", "")
if not secret:
msg = "ADMIN_KEY environment variable is required for key hashing"
raise RuntimeError(msg)
return hmac.new(secret.encode(), key.encode(), hashlib.sha256).hexdigest()
def generate_api_key() -> str:
"""Generate a random API key."""
return f"docsfy_{secrets.token_urlsafe(32)}"
In practice, that means:
- When a user is created or their key is rotated, the raw key is shown once and should be saved immediately.
- If you lose a key, rotate it. The server cannot show you the old one.
- Auto-generated keys start with
docsfy_. - If you change
ADMIN_KEY, existing database-backed user keys stop matching until those users are issued new keys.
Note: The admin UI and CLI both treat newly created or rotated credentials as one-time secrets: save them when they are shown.
Browser login and session cookies
The browser does not send your API key on every request after login. Instead, it exchanges username + api_key for a session cookie and then uses that cookie for normal browsing.
From frontend/src/pages/LoginPage.tsx:
await api.post<AuthResponse>('/api/auth/login', {
username,
api_key: password,
})
From src/docsfy/storage.py:
SESSION_TTL_SECONDS = 28800 # 8 hours
From src/docsfy/api/auth.py:
response.set_cookie(
"docsfy_session",
session_token,
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
max_age=SESSION_TTL_SECONDS,
)
What that means in practice:
- The cookie name is
docsfy_session. - Sessions last 8 hours.
- The cookie is
HttpOnly, so browser JavaScript cannot read it directly. - The cookie uses
SameSite=Strict. - The
Secureflag is controlled bySECURE_COOKIES. - Logout deletes the server-side session and clears the cookie.
The session token is opaque; it is not the raw API key. If a user is deleted or their key is rotated, their existing sessions stop working.
When you browse protected docs without a valid session, docsfy redirects HTML requests for /docs/... to /login. Protected API requests return 401 Unauthorized.
Note:
SECURE_COOKIES=trueis the default and is the right setting for HTTPS deployments. For local HTTP development, setSECURE_COOKIES=falseor the browser will not send the cookie back.
CLI and API clients
CLI and other non-browser clients usually skip the login endpoint and send the API key directly as a Bearer token.
From config.toml.example:
[servers.dev]
url = "http://localhost:8000"
username = "admin"
password = "<your-dev-key>"
From src/docsfy/cli/client.py:
self._client = httpx.Client(
base_url=self.server_url,
headers={"Authorization": f"Bearer {self.password}"},
timeout=30.0,
follow_redirects=False,
)
This is why the same secret works in both places:
- In the web UI, you type it into the Password field and receive a session cookie.
- In the CLI, it is stored as
passwordin~/.config/docsfy/config.tomland sent as a Bearer token.
The CLI configuration code writes that file with owner-only permissions.
Warning:
~/.config/docsfy/config.tomlcontains real credentials. Keep it private.
Roles and permissions
viewer
A viewer is read-only.
- Can sign in.
- Can view docs they own or docs an admin shared with them.
- Can download docs they are allowed to view.
- Can rotate their own API key.
- Cannot generate, regenerate, abort, or delete documentation.
- Cannot access the admin panel.
user
A user is a normal write-enabled account.
- Can do everything a
viewercan do. - Can generate docs from remote repository URLs.
- Can regenerate, abort, and delete their own variants.
- Can see their own projects plus any projects explicitly shared with them.
- Cannot access the admin panel.
- Cannot use local filesystem
repo_pathgeneration; that is admin-only.
admin
An admin has global visibility and user-management access.
- Can do everything a
usercan do. - Can see all projects across all owners.
- Can access the admin panel.
- Can create and delete users.
- Can rotate any user's API key.
- Can grant and revoke project access.
- Can generate from local filesystem paths (
repo_path).
Note: Database-backed admins and the built-in
adminaccount get the same admin permissions. The difference is rotation behavior: database-backed admins can rotate their own key in the app, while the built-inadminaccount is rotated by changingADMIN_KEY.Tip: Role checks are enforced on the server, not just hidden in the UI. For example,
viewerwrite requests are rejected even if someone manually calls the API.
Shared access is owner-scoped
By default, non-admin users only see their own projects. Admins can grant access to another user's project, but the grant is scoped to both the project name and the owner.
That owner scoping matters because two different users can generate docs for repositories with the same name. A grant to Alice's for-testing-only project does not automatically grant Bob's for-testing-only project.
From frontend/src/components/admin/AccessPanel.tsx:
await api.post(`/api/admin/projects/${encodeURIComponent(grantProject.trim())}/access`, {
username: grantUsername.trim(),
owner: grantOwner.trim(),
})
A grant applies to all variants of that project name for that owner. Revoking access removes those shared views again, and the restriction is enforced on direct URLs too, not just in the dashboard.
In practice:
- Admins see everything.
- Non-admins see their own projects.
- Non-admins also see projects listed in their access grants.
- If a user does not have access, project details, downloads, and docs URLs return
404rather than leaking that the project exists.
Tip: When an admin grants access, always use the correct owner. If multiple users generated the same repo name, the owner determines which copy is shared.
Key rotation
Database-backed users can rotate their own keys. Admins can also rotate keys on behalf of other users.
From src/docsfy/storage.py:
async def rotate_user_key(username: str, custom_key: str | None = None) -> str:
"""Generate or set a new API key for a user. Returns the raw new key."""
if custom_key:
validate_api_key(custom_key)
raw_key = custom_key
else:
raw_key = generate_api_key()
key_hash = hash_api_key(raw_key)
async with aiosqlite.connect(DB_PATH) as db:
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()
return raw_key
From src/docsfy/api/auth.py:
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,
)
Rotation behaves like this:
POST /api/auth/rotate-keyrotates the current database-backed user's key.POST /api/admin/users/{username}/rotate-keylets an admin rotate another user's key.- If you send
new_key, it must be at least 16 characters long. - If you omit
new_key,docsfygenerates a newdocsfy_...key. - The old key stops working immediately.
- All active sessions for that user are invalidated.
- The self-service browser flow clears the current session cookie, so the user must log in again with the new key.
- Rotation responses use
Cache-Control: no-storebecause they contain sensitive credentials.
viewer users can rotate their own keys just like user users. Database-backed admins can too.
Warning: The built-in
adminaccount cannot use the rotate-key endpoint. If you are logged in asadminviaADMIN_KEY, rotate that credential by changing theADMIN_KEYenvironment variable and restarting the server.Warning: Rotating
ADMIN_KEYis a global event for database-backed users, because their stored API-key hashes are derived from it. Plan to reissue or rotate user keys after changingADMIN_KEY.
Practical guidance
Use the built-in admin account to bootstrap the system, then create database-backed users for day-to-day access.
A good pattern is:
- Keep
ADMIN_KEYfor break-glass access and initial administration. - Create named database users for humans and service accounts.
- Give read-only people the
viewerrole. - Give day-to-day writers the
userrole. - Grant project access explicitly when someone needs to see another owner's docs.
- Rotate lost or exposed keys instead of trying to recover them, because raw keys are not stored.
If you keep those rules in mind, docsfy's auth model stays simple: one secret per account, role-based permissions on top, and short-lived browser sessions for the UI.