First Documentation Run
This guide walks you from first login to a generated, browsable docs site in docsfy.
1) Configure your environment
docsfy reads settings from .env (pydantic-settings in src/docsfy/config.py) and requires ADMIN_KEY at startup.
# .env.example
# REQUIRED - Admin key for user management (minimum 16 characters)
ADMIN_KEY=your-secure-admin-key-here-min-16-chars
# AI Configuration
AI_PROVIDER=claude
# [1m] = 1 million token context window, this is a valid model identifier
AI_MODEL=claude-opus-4-6[1m]
AI_CLI_TIMEOUT=60
# Set to false for local HTTP development
# SECURE_COOKIES=false
# src/docsfy/main.py
if not settings.admin_key:
logger.error("ADMIN_KEY environment variable is required")
raise SystemExit(1)
if len(settings.admin_key) < 16:
logger.error("ADMIN_KEY must be at least 16 characters long")
raise SystemExit(1)
Warning: If you run over plain HTTP (for example
http://localhost:8000), setSECURE_COOKIES=falsein.env. Cookies aresecure=Trueby default, so login sessions will not stick on HTTP.
2) Start docsfy
Recommended: Docker Compose
# docker-compose.yaml
services:
docsfy:
build: .
ports:
- "8000:8000"
env_file: .env
volumes:
- ./data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
Run:
docker compose up --build
The container image installs AI CLIs during build:
# Dockerfile
RUN /bin/bash -o pipefail -c "curl -fsSL https://claude.ai/install.sh | bash"
RUN /bin/bash -o pipefail -c "curl -fsSL https://cursor.com/install | bash"
RUN mkdir -p /home/appuser/.npm-global \
&& npm config set prefix '/home/appuser/.npm-global' \
&& npm install -g @google/gemini-cli
Local run (without Docker)
pyproject.toml defines a CLI entry point:
[project.scripts]
docsfy = "docsfy.main:run"
So after dependency setup, you can run:
uv run docsfy
docsfy.main:run defaults to 127.0.0.1:8000.
3) Log in
Open: http://localhost:8000/login
The login form uses username + API key (labeled “Password” in the UI):
<!-- src/docsfy/templates/login.html -->
<label for="username">Username</label>
<input type="text" id="username" name="username" ...>
<label for="api_key">Password</label>
<input type="password" id="api_key" name="api_key" ...>
<p>Admin login: username <strong>admin</strong> with the admin password.</p>
Backend auth logic:
# src/docsfy/main.py
if username == "admin" and api_key == settings.admin_key:
is_admin = True
authenticated = True
else:
user = await get_user_by_key(api_key)
if user and user["username"] == username:
authenticated = True
is_admin = user.get("role") == "admin"
Session cookies are set as HTTP-only, strict same-site, 8-hour TTL:
response.set_cookie(
"docsfy_session",
session_token,
httponly=True,
samesite="strict",
secure=settings.secure_cookies,
max_age=SESSION_TTL_SECONDS,
)
Note:
SESSION_TTL_SECONDSis28800(8 hours) insrc/docsfy/storage.py.
4) Generate your first docs site
After login, go to dashboard (/) and use Generate Documentation.
<!-- src/docsfy/templates/dashboard.html -->
<label for="gen-repo-url">Repository URL</label>
<input type="url" id="gen-repo-url" ... placeholder="https://github.com/org/repo" required>
<select id="gen-provider" class="form-select">
<option value="claude">claude</option>
<option value="gemini">gemini</option>
<option value="cursor">cursor</option>
</select>
<input type="text" class="form-input" id="gen-model" ...>
<input type="checkbox" id="gen-force">
<button type="submit" class="btn btn-primary" id="gen-submit">Generate</button>
Frontend payload sent to the API:
// src/docsfy/templates/dashboard.html
var body = {
repo_url: repoUrl,
ai_provider: provider,
force: force
};
if (model) body.ai_model = model;
fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
})
Server request model constraints:
# src/docsfy/models.py
if not self.repo_url and not self.repo_path:
raise ValueError("Either 'repo_url' or 'repo_path' must be provided")
if self.repo_url and self.repo_path:
raise ValueError("Provide either 'repo_url' or 'repo_path', not both")
https_pattern = r"^https?://[\w.\-]+/[\w.\-]+/[\w.\-]+(\.git)?$"
ssh_pattern = r"^git@[\w.\-]+:[\w.\-]+/[\w.\-]+(\.git)?$"
Generation returns 202 with project name inferred from repo URL:
# src/docsfy/main.py
return {"project": project_name, "status": "generating"}
# tests/test_main.py
response = await client.post("/api/generate", json={"repo_url": "https://github.com/org/repo.git"})
assert response.status_code == 202
assert response.json()["project"] == "repo"
assert response.json()["status"] == "generating"
Warning: Admin-only restriction applies to
repo_path. Non-admin users get403for local path generation.Warning: Repo URLs resolving to localhost/private networks are rejected (
_reject_private_urlinsrc/docsfy/main.py).
5) Monitor generation
From the dashboard
A generating variant shows a progress bar and a status link:
<!-- src/docsfy/templates/dashboard.html -->
<span class="progress-text">Generating...</span>
<a href="/status/{{ repo_name }}/{{ variant.ai_provider | urlencode }}/{{ variant.ai_model | urlencode }}"
target="_blank" class="status-link">View progress →</a>
Dashboard polling behavior:
// src/docsfy/templates/dashboard.html
var statusPollInterval = null; // Slow poll for status changes (10s)
var progressPollInterval = null; // Fast poll for progress updates (5s)
statusPollInterval = setInterval(pollStatusChanges, 10000);
progressPollInterval = setInterval(pollProgressUpdates, 5000);
From the status page
Status page polling behavior:
// src/docsfy/templates/status.html
var POLL_INTERVAL_MS = 3000;
pollTimer = setInterval(pollProject, POLL_INTERVAL_MS);
Generation stage updates are written by backend as:
cloningplanningincremental_planninggenerating_pagesrenderingup_to_date(when no changes)
(from _run_generation and _generate_from_path in src/docsfy/main.py)
Ready-state messaging:
<!-- src/docsfy/templates/status.html -->
<span id="success-text">
{% if project.current_stage == 'up_to_date' %}
Documentation is already up to date — no changes since last generation.
{% else %}
Documentation generated successfully!
{% endif %}
</span>
Tip: If you manually type URLs, always include provider/model segments from the current variant. The dashboard/status buttons build the correct URL for you.
6) Open and download your generated docs
When status is ready, use View Documentation or Download:
<!-- src/docsfy/templates/status.html -->
<a href="/docs/{{ project.name }}/{{ project.ai_provider }}/{{ project.ai_model }}/"
target="_blank" class="btn btn-primary" id="btn-view-docs">View Documentation</a>
<a href="/api/projects/{{ project.name }}/{{ project.ai_provider }}/{{ project.ai_model }}/download"
class="btn btn-secondary" id="btn-download">Download</a>
Routes:
# src/docsfy/main.py
@app.get("/docs/{project}/{provider}/{model}/{path:path}") # variant-specific
@app.get("/docs/{project}/{path:path}") # latest ready variant
Integration tests confirm both variant and latest routes:
# tests/test_integration.py
response = await client.get("/docs/test-repo/claude/opus/index.html")
assert response.status_code == 200
response = await client.get("/docs/test-repo/index.html")
assert response.status_code == 200
response = await client.get("/api/projects/test-repo/claude/opus/download")
assert response.headers["content-type"] == "application/gzip"
7) Where generated files are stored
Storage path is owner/project/provider/model scoped:
# src/docsfy/storage.py
return PROJECTS_DIR / safe_owner / _validate_name(name) / ai_provider / ai_model
Site directory:
# src/docsfy/storage.py
return get_project_dir(name, ai_provider, ai_model, owner) / "site"
Renderer output includes:
index.html<slug>.html<slug>.mdsearch-index.jsonllms.txtllms-full.txt.nojekyllassets/*
# src/docsfy/renderer.py
(output_dir / "index.html").write_text(index_html, encoding="utf-8")
(output_dir / f"{slug}.html").write_text(page_html, encoding="utf-8")
(output_dir / f"{slug}.md").write_text(md_content, encoding="utf-8")
(output_dir / "search-index.json").write_text(json.dumps(search_index), encoding="utf-8")
(output_dir / "llms.txt").write_text(llms_txt, encoding="utf-8")
(output_dir / "llms-full.txt").write_text(llms_full_txt, encoding="utf-8")
With Docker Compose, these are persisted under local ./data because of ./data:/data.
8) Optional sanity check after first run
Local test command defined in tox.toml:
[env.unittests]
deps = ["uv"]
commands = [["uv", "run", "--extra", "dev", "pytest", "-n", "auto", "tests"]]
Note: This repository currently defines local quality gates (
tox,pre-commit) but does not include a checked-in GitHub Actions workflow file.