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), set SECURE_COOKIES=false in .env. Cookies are secure=True by default, so login sessions will not stick on HTTP.

2) Start docsfy

# 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_SECONDS is 28800 (8 hours) in src/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 get 403 for local path generation.

Warning: Repo URLs resolving to localhost/private networks are rejected (_reject_private_url in src/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 &rarr;</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:

  • cloning
  • planning
  • incremental_planning
  • generating_pages
  • rendering
  • up_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>.md
  • search-index.json
  • llms.txt
  • llms-full.txt
  • .nojekyll
  • assets/*
# 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.