Backend architecture (FastAPI)¶
backend/
├── pyproject.toml, uv.lock, alembic.ini
├── alembic/
│ ├── env.py
│ └── versions/ # migrations (0001 → 0004)
└── app/
├── main.py # FastAPI entry point
├── config.py # AppSettings (pydantic-settings)
├── database.py # async engine + session
├── auth.py # JWT cookies, get_current_user/admin dependencies
├── security.py # middlewares (CSRF, rate-limit, headers)
├── proxy.py # client IP resolution behind a reverse proxy
├── exceptions.py # normalized error handlers
├── helpers.py # utilities (e.g. anti path-traversal)
├── models.py # SQLModel entities (tables)
├── schemas.py # Pydantic schemas (API input/output)
├── routers/ # one file per functional domain
├── services/ # reusable business logic
├── static/swagger/ # self-hosted Swagger UI
└── templates/ # HTML emails (en/fr/es)
main.py¶
This is the application's entry point (app.main:app). It assembles everything below, in this order:
1. App creation¶
app = FastAPI(
title="WireGuard UI",
version=app_settings.app_version,
lifespan=lifespan,
docs_url=None, # Swagger handled manually (self-hosted)
redoc_url=None,
openapi_url="/api/openapi.json" if app_settings.swagger_enabled else None,
)
2. Middlewares (in the order added)¶
| Middleware | Role |
|---|---|
SecurityHeadersMiddleware |
Adds security headers (CSP, HSTS, X-Frame-Options, etc.) to every response. |
CsrfMiddleware |
Double-submit cookie CSRF protection: for unsafe methods (POST/PUT/PATCH/DELETE) on a cookie-authenticated /api/* route, requires the X-CSRF-Token header to match the csrf_token cookie. |
RateLimitMiddleware |
Limits the number of requests per IP (sliding window), with a stricter threshold for /api/auth/login and /api/auth/token. |
Full details: Authentication & security.
3. Exception handlers¶
RequestValidationError and StarletteHTTPException are intercepted and reformatted into a normalized JSON structure {code, message, details} (exceptions.py), so that the Angular frontend has a predictable error format.
4. Lifecycle (lifespan)¶
@asynccontextmanager
async def lifespan(app: FastAPI):
await seed_initial_data()
if app_settings.wg_autostart:
await auto_start_wireguard()
yield
await engine.dispose()
seed_initial_data()(services/seed.py): creates the roles, the initial administrator, and the default settings if they don't already exist. Run on every startup (idempotent).auto_start_wireguard(): ifWIREGUARD_AUTOSTARTis enabled, writes the server configuration (write_server_config) and starts the WireGuard service (start_service). On failure, up to 3 attempts spaced 5 seconds apart, then gives up with an error log — without blocking API startup.- On shutdown,
engine.dispose()cleanly closes the database connection pool.
5. Routers¶
Each router is mounted with an /api/<domain> prefix (full details in Routers (endpoints)):
app.include_router(auth.router, prefix="/api/auth")
app.include_router(clients.router, prefix="/api/clients")
app.include_router(server.router, prefix="/api/server")
app.include_router(settings.router, prefix="/api/settings")
app.include_router(oidc.router, prefix="/api/oidc")
app.include_router(status.router, prefix="/api/status")
app.include_router(users.router, prefix="/api/users")
app.include_router(smtp.router, prefix="/api/smtp")
app.include_router(audit.router, prefix="/api/audit")
app.include_router(pat.router, prefix="/api/pat")
6. Endpoints outside routers¶
GET /api/health— simple healthcheck, excluded from rate limiting.GET /api/docs— self-hosted Swagger UI (no CDN dependency), active only ifSWAGGER_ENABLED=true.GET /{full_path:path}— must stay last: serves the Angular build (frontend/dist/wireguard-ui/browser, copied in production tofrontend/relative tobackend/app/). Usesresolve_safe_path()to prevent any access outside the SPA folder (path traversal), falling back toindex.htmlto let the Angular router handle client-side routes.
config.py¶
AppSettings(BaseSettings) class loaded once via get_settings() (@lru_cache) and exposed as the app_settings singleton. See the full variable reference in Environment variables.
Notable point: _resolve_secret_key() generates and persists a random secret key in DATA_DIR/secret_key if SECRET_KEY is not explicitly provided, so sessions stay valid across restarts without exposing a known default value.
database.py¶
Asynchronous SQLAlchemy engine (create_async_engine, aiosqlite or asyncpg driver) and a session factory (AsyncSessionLocal). The get_db() FastAPI dependency provides one session per request, with automatic rollback on exception.
proxy.py¶
Resolves the real client IP address (client_ip(request)), taking X-Forwarded-For into account only if the request comes from an IP listed in TRUSTED_PROXIES. Used by rate limiting and HTTPS detection (to mark the session cookie Secure).
Models and schemas¶
models.py: SQLModel table entities (persisted in the database) — see Database & migrations.schemas.py: non-table Pydantic schemas used for request validation and API response formatting (e.g.ClientCreate,UserResponse,AuditLogPage…), grouped by functional domain.
HTTP request diagram¶
flowchart TD
Req["HTTP Request"] --> SH[SecurityHeadersMiddleware]
SH --> CSRF[CsrfMiddleware]
CSRF --> RL[RateLimitMiddleware]
RL --> Router["Relevant router (/api/...)"]
Router --> Dep["FastAPI dependencies\n(get_db, get_current_user/admin)"]
Dep --> Service["Business service"]
Service --> DB[(Database)]
Service --> WG["wireguard.py (subprocess)"]
Router --> Resp["JSON response\n(schemas.py)"]