# authentik — Admin & Developer Guide

This guide covers the architecture, source organization, deployment, configuration internals, security posture, performance tuning, and extension points of **authentik** version `2026.8.0-rc1` (commit `9e75f62`, 2026-05-28). It targets operators running authentik in production, developers extending it, and security engineers vetting it for deployment.

For end-user installation and operation, see the **User Guide**.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [System Architecture](#2-system-architecture)
3. [Repository Structure](#3-repository-structure)
4. [Module Inventory](#4-module-inventory)
5. [Request Lifecycle](#5-request-lifecycle)
6. [The Flow Engine](#6-the-flow-engine)
7. [Provider Implementations](#7-provider-implementations)
8. [Outpost Architecture](#8-outpost-architecture)
9. [The Blueprint System](#9-the-blueprint-system)
10. [Configuration Reference](#10-configuration-reference)
11. [Database Schema and Migrations](#11-database-schema-and-migrations)
12. [Background Workers and Scheduling](#12-background-workers-and-scheduling)
13. [API Surface (`/api/v3/`)](#13-api-surface-apiv3)
14. [Frontend Architecture (`web/`)](#14-frontend-architecture-web)
15. [Operations Runbooks](#15-operations-runbooks)
16. [Security Analysis](#16-security-analysis)
17. [Performance Tuning](#17-performance-tuning)
18. [Code Quality and Patterns](#18-code-quality-and-patterns)
19. [Testing Strategy](#19-testing-strategy)
20. [Suggested Improvements](#20-suggested-improvements)
21. [Contributing](#21-contributing)
22. [Quick Reference](#22-quick-reference)

---

## 1. Executive Summary

| Attribute | Value |
|---|---|
| Repo | [github.com/goauthentik/authentik](https://github.com/goauthentik/authentik) |
| Commit documented | `9e75f62` (2026-05-28) |
| Version | `2026.8.0-rc1` |
| Total source | 10,403,381 chars across 5,814 files |
| Languages | Python 3.14 (Django 5.2), Go, Rust (axum), TypeScript (Lit) |
| License model | MIT (core) + EE (`authentik/enterprise/` + `web/src/admin/enterprise/`) + CC BY-SA 4.0 (`website/`) + proprietary (`website/static/img/`) |
| Primary deployment | Docker Compose, Kubernetes Helm, AWS CloudFormation, DigitalOcean Marketplace |
| Sole stateful dependency | PostgreSQL (no Redis required — `django-postgres-cache` + `django-channels-postgres` use LISTEN/NOTIFY) |
| External binaries | 6 (1 Python server entry + 5 Go outpost binaries) |
| Default network ports | 9000 (HTTP), 9443 (HTTPS), 3389/6636 (LDAP/LDAPS), 1812 (RADIUS UDP), 9300 (metrics), 9900 (debug) |

**What authentik is**: a multi-protocol Identity Provider implemented as a Django REST framework backend (`authentik/`) with a Web Components admin UI (`web/`), augmented by purpose-built Go outposts (`cmd/`, `internal/outpost/`) for protocol families that need raw-socket handling (LDAP, RADIUS, RDP/SSH gateway via RAC, and HTTP forward-auth via the Proxy provider). The core is dual-licensed: the MIT-licensed open core and an EE add-on tier (`authentik/enterprise/`) that adds SSF, MS Entra/Google Workspace sync, audit log, MTLS stage, account lockdown, RAC, unique-password policy, and a few connectors.

**Why three runtimes (Python + Go + Rust)?** Python/Django handles the business logic, ORM, admin REST API, and flow execution where developer velocity matters most. Go handles outpost wire protocols where latency and concurrent connection count matter (a single LDAP outpost handles thousands of concurrent binds; Django could not). Rust (in `packages/ak-axum/` and `packages/ak-common/`) is reserved for the high-performance API surface and the PROXY-protocol-aware reverse-proxy frontend; it's the newest layer (introduced for the Server v2 refactor) and currently a thin slice of the surface.

---

## 2. System Architecture

```text
                    ┌─────────────────────────────────────────────────────────┐
                    │                          USERS                           │
                    └──┬───────────────────────────┬──────────────────────────┘
                       │ HTTPS                     │ LDAP / RADIUS / RDP / SSH
                       ▼                           ▼
            ┌──────────────────────┐    ┌────────────────────────┐
            │  Reverse Proxy       │    │    Outposts (Go)        │
            │  (Traefik/Nginx/Caddy)│   │  ldap / radius / rac /  │
            └──────────┬───────────┘    │  proxy (separate)        │
                       │ HTTP            └────────┬───────────────┘
                       ▼                           │  websocket+REST
            ┌──────────────────────┐               │
            │  authentik server    │◄──────────────┘
            │  ┌────────────────┐  │
            │  │ Web (TS/Lit)   │  │  /if/admin/, /if/user/, /if/flow/
            │  │ Static assets  │  │
            │  └────────────────┘  │
            │  ┌────────────────┐  │
            │  │ Django + DRF   │  │  /api/v3/*
            │  │ ASGI (uvicorn) │  │
            │  └────────┬───────┘  │
            │  ┌────────▼───────┐  │
            │  │ Embedded       │  │  inline Proxy outpost (Go process)
            │  │ Outpost (Go)   │  │  shares port 9000 with router
            │  └────────────────┘  │
            └──────────┬───────────┘
                       │
                       │       ┌─────────────────────────┐
                       │       │   authentik worker      │
                       │       │   (Django + dramatiq)   │
                       │       │   - background tasks    │
                       │       │   - email send          │
                       │       │   - sync jobs           │
                       │       │   - event processors    │
                       │       └────────────┬────────────┘
                       │                    │
                       ▼                    ▼
            ┌──────────────────────────────────────┐
            │            PostgreSQL                 │
            │  - all object state                   │
            │  - sessions (authenticatedsession)    │
            │  - cache (django-postgres-cache)      │
            │  - pubsub (LISTEN/NOTIFY via          │
            │     django-channels-postgres)         │
            │  - task queue (django-dramatiq-postgres) │
            └──────────────────────────────────────┘
```

### Components

| Component | Process | Language | Mounted volumes |
|---|---|---|---|
| **server** | `python -m gunicorn -k uvicorn.workers.UvicornWorker authentik.root.asgi:application` | Python 3.14 | `/data`, `/blueprints`, `/certs`, `/media` |
| **worker** | `python -m dramatiq authentik.tasks` (via `lifecycle/worker.py`) | Python 3.14 | `/blueprints`, `/templates`, `/data`, `/var/run/docker.sock` (optional) |
| **embedded outpost** | child of server; Go binary `authentik` | Go | n/a (in-container) |
| **proxy outpost** (separate) | `authentik-proxy` | Go | n/a |
| **ldap outpost** | `authentik-ldap` | Go | optional `/certs` |
| **radius outpost** | `authentik-radius` | Go | n/a |
| **rac outpost** | `authentik-rac` | Go | n/a |
| **postgresql** | `postgres` | C | `/var/lib/postgresql/data` |

### Why the embedded outpost?

For small installs, running a separate Go process for the Proxy provider is overkill. The server container starts a Go binary as a goroutine that shares port 9000 via a UNIX domain socket. The lightweight router in `internal/web/` decides per-request whether to route to the Django ASGI app (most paths), the embedded outpost (forward-auth paths under `/outpost.goauthentik.io/`), or to static asset serving. See `cmd/server/server.go:65-100` for the `attemptProxyStart` loop that brings up the embedded outpost once the API is healthy.

### Why PostgreSQL-only?

Older versions of authentik used Redis. The current architecture replaces Redis with three PostgreSQL-based libraries:

- **`django-postgres-cache`** (`packages/django-postgres-cache/`) — a Django cache backend that uses a PostgreSQL table with a TTL column.
- **`django-channels-postgres`** (`packages/django-channels-postgres/`) — a Django Channels layer using `LISTEN/NOTIFY` for pub/sub, eliminating the need for a Redis channel layer.
- **`django-dramatiq-postgres`** (`packages/django-dramatiq-postgres/`) — a `dramatiq` broker implementation that uses a PostgreSQL queue table.

The trade-off: PostgreSQL pub/sub has higher latency than Redis (~20-50 ms vs <5 ms) and lower throughput. But for an IdP, this is negligible compared to the simplification: one stateful service, one backup target, one network firewall hole. Larger installs can still configure an external `cache.url` and `channel.url` in `default.yml` to point at Redis if they prefer.

---

## 3. Repository Structure

```text
authentik/                                  # repo root
├── Cargo.toml                              # Rust workspace (ak-axum, ak-common, client-rust, docsmg)
├── Cargo.lock
├── Makefile                                # 'make gen-dev-config', 'make docs', 'make test', etc.
├── README.md                               # marketing-tier overview (53 lines)
├── SECURITY.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md
├── LICENSE                                 # MIT (core)
├── manage.py                               # Django entrypoint (Python)
├── pyproject.toml                          # Python 3.14, 60+ runtime deps
├── package.json                            # npm workspace root
├── package-lock.json
├── go.mod, go.sum                          # Go modules root for cmd/* + internal/*
├── build.rs                                # Cargo build script (PyO3 bridge)
├── rust-toolchain.toml                     # pinned Rust toolchain
├── schema.yml                              # generated OpenAPI 3.0 spec
├── cspell.config.jsonc                     # spellcheck config (dictionaries under web/e2e/utils/)
│
├── authentik/                              # ★ Python application (Django)
│   ├── root/                               # Django project: settings.py, asgi.py, urls.py
│   ├── api/                                # ViewSets for /api/v3/*, schema customization
│   ├── lib/                                # shared utilities: config loader, expressions, logging
│   │   └── default.yml                     # ★ canonical runtime configuration
│   ├── core/                               # User, Application, Source, Group, Token, Brand models
│   ├── flows/                              # Flow engine: Flow, Stage, FlowStageBinding, FlowPlanner
│   ├── stages/                             # 22 stage implementations (identification, password, ...)
│   ├── providers/                          # 8 provider implementations (saml, oauth2, ldap, radius, proxy, rac, scim, ldap)
│   ├── sources/                            # 7 source implementations (ldap, oauth, saml, kerberos, scim, plex, telegram)
│   ├── policies/                           # 7 policy implementations + base classes
│   ├── outposts/                           # Outpost model + Docker/Kubernetes controllers
│   ├── blueprints/                         # YAML import/export + apply engine
│   ├── events/                             # Event model + notification system + context processors
│   ├── crypto/                             # X.509 cert generation/import (CertificateKeyPair model)
│   ├── brands/                             # Brand (multi-tenancy by domain) model + views
│   ├── tenants/                            # django-tenants integration (different from Brands)
│   ├── tasks/                              # Dramatiq task registry + schedules
│   ├── rbac/                               # Django-Guardian based RBAC
│   ├── admin/                              # admin REST APIs (apps, files, settings, version)
│   ├── recovery/                           # `ak create_recovery_key`, `ak create_admin_group`
│   ├── commands/                           # generic shell commands (`ak shell_plus` etc.)
│   ├── endpoints/                          # Endpoint device model (EE)
│   ├── common/                             # shared OAuth/SAML XML constants
│   └── enterprise/                         # ★ EE-licensed code (SSF, Entra/GWS sync, audit, MTLS, RAC, account_lockdown, unique_password, reports)
│
├── cmd/                                    # ★ Go binaries (one per outpost type)
│   ├── server/                             # main authentik server binary + embedded outpost
│   ├── proxy/                              # Forward-auth proxy outpost
│   ├── ldap/                               # LDAP outpost
│   ├── radius/                             # RADIUS outpost
│   └── rac/                                # Remote Access Connection outpost
│
├── internal/                               # Go shared libraries
│   ├── common/                             # PreRun, version info, exit channel
│   ├── config/                             # config.yml loader (Go side)
│   ├── constants/                          # version + user-agent string
│   ├── crypto/                             # FIPS-aware backend selection
│   ├── debug/                              # pprof + dump_config debug HTTP server (port 9900)
│   ├── gounicorn/                          # gunicorn supervisor (used inside the Python container)
│   ├── outpost/                            # outpost framework: API controller, healthcheck, entrypoint
│   │   ├── ak/                             # AuthAPIController, healthchecks, websocket loop
│   │   ├── ldap/                           # LDAP outpost handler (bind, search, group, metrics)
│   │   ├── proxyv2/                        # Proxy outpost (the modern one — v1 was retired)
│   │   ├── radius/                         # RADIUS outpost (PAP, CHAP, MS-CHAPv2)
│   │   ├── rac/                            # RAC outpost (Guacamole-protocol bridge)
│   │   └── ak/healthcheck/                 # shared /health/live/, /health/ready/
│   ├── utils/                              # web (TLS, transports), sentry helpers, unix
│   ├── crypto/backend/                     # FIPS backend factory
│   └── web/                                # WebServer, BrandTLS lookup, routing
│
├── packages/                               # ★ npm + Cargo workspaces (shared libraries)
│   ├── ak-axum/                            # Rust axum extensions (PROXY-protocol, extractors)
│   ├── ak-common/                          # Rust common helpers (config, tokio extensions)
│   ├── ak-guardian/                        # Python: django-guardian fork with management commands
│   ├── client-go/                          # Go SDK (auto-generated from schema.yml)
│   ├── client-ts/                          # TypeScript SDK (auto-generated)
│   ├── client-rust/                        # Rust SDK (auto-generated)
│   ├── django-channels-postgres/           # PostgreSQL pub/sub channel layer
│   ├── django-dramatiq-postgres/           # PostgreSQL task broker for dramatiq
│   ├── django-postgres-cache/              # PostgreSQL cache backend
│   ├── docusaurus-config/                  # shared Docusaurus theme config
│   ├── esbuild-plugin-live-reload/         # dev-only esbuild plugin
│   ├── eslint-config/, prettier-config/, tsconfig/  # shared lint/format/TS config
│   └── logger-js/                          # shared JS logger
│
├── web/                                    # ★ TypeScript frontend (Lit web components)
│   ├── src/
│   │   ├── admin/                          # Admin interface (entrypoint: admin/index.entrypoint.ts)
│   │   ├── user/                           # User interface (entrypoint: user/index.entrypoint.ts)
│   │   ├── flow/                           # Flow executor UI (entrypoint: flow/index.entrypoint.ts)
│   │   ├── rac/                            # RAC client (entrypoint: rac/index.entrypoint.ts)
│   │   ├── elements/                       # Shared web components
│   │   ├── common/                         # API client glue, helpers, ws, errors
│   │   ├── components/                     # Form fields, search-ql, hints
│   │   └── standalone/                     # api-browser (Swagger UI), loading splash
│   ├── packages/sfe/                       # Simplified Flow Executor for IE-11 / MSEdge-18 (Office 365)
│   ├── packages/core/                      # Browser shims (env, paths, scripting, version)
│   ├── packages/lex/                       # Tokenizer for the search-ql component
│   ├── packages/formdata-polyfill/         # IE-11 polyfill
│   ├── packages/node-domexception/         # Node DOMException polyfill
│   ├── e2e/                                # Playwright e2e tests
│   ├── bundler/                            # esbuild config + vite-plugin-lit-css
│   └── tools/                              # Refactor scripts (kept for posterity)
│
├── website/                                # ★ Docusaurus documentation site (goauthentik.io)
│   ├── docs/                               # User-facing docs
│   ├── integrations/                       # Per-app integration recipes (150+)
│   ├── api/                                # API browser embed
│   ├── docusaurus-theme/                   # Custom theme components
│   └── scripts/docsmg/                     # Rust CLI for bulk docs migration
│
├── blueprints/                             # ★ Default blueprints applied on startup
│   ├── default/                            # Required blueprints (default flows, providers, etc.)
│   ├── example/                            # Reference blueprints to clone/adapt
│   ├── system/                             # Internal-use blueprints
│   ├── migrations/                         # Schema-evolution blueprints
│   └── testing/                            # Used by CI only
│
├── locale/                                 # ★ i18n translations (django-translatable + lit-localize)
│   ├── cs_CZ/, de_DE/, en/, es_ES/, fr_FR/, it_IT/, ja_JP/, ko_KR/, nl_NL/,
│   │   no_NO/, pl_PL/, pt_BR/, pt_PT/, ru_RU/, tr_TR/, zh-Hans/, zh-Hant/,
│   │   bg_BG/, fi_FI/                      # each contains LC_MESSAGES/*.po
│
├── lifecycle/                              # ★ container init scripts
│   ├── ak                                  # `ak` shell wrapper (sources venv, exec manage.py)
│   ├── ak.py                               # Python entrypoint
│   ├── gunicorn.conf.py                    # gunicorn worker config
│   ├── migrate.py                          # idempotent migration runner
│   ├── wait_for_db.py                      # pg_isready loop
│   ├── worker.py, worker_process.py        # dramatiq worker entrypoint
│   ├── system_migrations/                  # one-shot fix-up scripts run on startup
│   ├── aws/                                # CDK app for AWS CloudFormation
│   └── container/                          # Docker entrypoint scripts
│
├── tests/                                  # ★ test harnesses
│   ├── e2e/                                # Playwright + docker-compose stacks
│   ├── integration/                        # Python integration tests
│   ├── openid_conformance/                 # OIDC compliance suite
│   └── geoip/                              # GeoIP test data
│
├── schemas/                                # JSON Schemas for blueprints, etc.
├── scripts/                                # node scripts (build helpers)
└── src/                                    # Rust workspace (axum frontend, metrics, worker)
    ├── main.rs                             # axum entry (Server v2, in beta)
    ├── server/                             # axum router
    ├── worker/                             # tokio worker harness
    └── metrics/                            # Prometheus middleware
```

> [!IMPORTANT]
> The Rust workspace under `src/` is a **next-generation frontend** that is in active development. Production deployments today still use the Python/Go architecture. The Rust `src/main.rs` is destined to replace `cmd/server/main.go` (Go) as the high-performance HTTP frontend, terminating TLS, applying PROXY-protocol, and reverse-proxying to a Unix-domain socket served by Django/uvicorn. Treat it as a forward-looking surface; the canonical entry remains `cmd/server/`.

---

## 4. Module Inventory

The briefing extracted **168 module files** (most are 1:1 with a Python package). The top-level Python modules under `authentik/`, with their primary responsibility:

| Module | Purpose | Key files |
|---|---|---|
| `authentik/root` | Django project config | `settings.py` (1500+ lines), `asgi.py`, `urls.py`, `routing.py`, `celery.py` (legacy) |
| `authentik/api` | DRF ViewSets & schema generation | `v3/urls.py`, `views/__init__.py`, `search.py`, custom schema patches in `schema/` |
| `authentik/lib` | Shared utilities | `default.yml` (config baseline), `config.py` + `config.go` (twin loaders), `expression/` (Python-expression evaluator), `generators.py`, `logging.py` |
| `authentik/core` | Identity primitives | `models.py` (User, Application, Source, Group, Token, AuthenticatedSession, PropertyMapping), `api/` (CoreViewSets) |
| `authentik/flows` | Flow execution engine | `models.py` (Flow, Stage, FlowStageBinding, FlowToken), `planner.py` (FlowPlan, FlowPlanner — the heart), `views/executor.py`, `markers.py` (reevaluate, stage skip) |
| `authentik/stages/*` | 22 stage types | Each is its own Django app with `models.py`, `stage.py` (StageView subclass), `forms.py`, `tests/` |
| `authentik/providers/*` | 8 provider types | `oauth2/`, `saml/`, `ldap/`, `radius/`, `proxy/`, `rac/`, `scim/` (plus 4 EE under enterprise/providers/) |
| `authentik/sources/*` | 7 source types | `oauth/`, `saml/`, `ldap/`, `kerberos/`, `scim/`, `plex/`, `telegram/` |
| `authentik/policies/*` | 7 policy types + base | `expression/`, `reputation/`, `password/`, `expiry/`, `event_matcher/`, `geoip/`, `dummy/` + EE `unique_password/` |
| `authentik/outposts` | Outpost orchestration | `models.py` (Outpost, OutpostServiceConnection), `controllers/` (docker.py, k8s/), `apps.py` (auto-discovery) |
| `authentik/blueprints` | YAML import/export engine | `v1/` (loader, exporter, tags: `!Env`, `!Find`, `!KeyOf`, `!Format`, `!If`, `!Context`), `apps.py` (file watcher), `management/commands/apply_blueprint.py` |
| `authentik/events` | Event sourcing | `models.py` (Event, Notification, NotificationTransport, NotificationRule), `context_processors/` (geoip, asn), `monitored_tasks.py`, notification dispatchers per transport |
| `authentik/crypto` | X.509 management | `models.py` (CertificateKeyPair), `builder.py` (CSR generation), `management/commands/import_certificate.py` |
| `authentik/brands` | Multi-domain branding | `models.py` (Brand), `views.py` (brand resolution middleware) |
| `authentik/tenants` | Schema-per-tenant (advanced) | django-tenants integration; defaults disabled |
| `authentik/tasks` | Task registry + schedules | `apps.py` (dramatiq autodiscovery), `models.py` (TaskSchedule), schedules expressed as cron-like specs |
| `authentik/rbac` | Object-level permissions | `models.py` (Role, InitialPermissions), `api/` (RBAC ViewSets) wraps `django-guardian` |
| `authentik/admin` | Admin REST + system info | `api/applications.py`, `api/files/` (storage backends), `api/settings.py`, `api/version.py` |
| `authentik/recovery` | Break-glass tooling | `management/commands/create_recovery_key.py`, `create_admin_group.py`, `views.py` (token consumption) |
| `authentik/endpoints` | Endpoint devices (EE) | `models.py` (Endpoint, EndpointDevice), connectors under `endpoints/connectors/` |
| `authentik/enterprise` | EE-licensed code | `audit/` (tamper-evident events), `providers/` (entra, gws, ssf, ws_federation), `stages/` (mtls, source, account_lockdown, authenticator_endpoint_gdtc), `policies/unique_password/`, `reports/`, `endpoints/connectors/` (fleet, google_chrome, agent) |

### Go internals (`internal/`)

| Package | Purpose |
|---|---|
| `internal/common` | Init, PreRun (cobra hook), exit-signal channel |
| `internal/config` | Reads `/etc/authentik/config.yml` + env overrides (Go-side twin of `authentik/lib/config.py`) |
| `internal/constants` | Version string assembly (`GIT_BUILD_HASH` env var read; see `extracted/config_keys.tsv:15`) |
| `internal/crypto` | FIPS-aware factory: returns AWS-LC-RS when `compliance.fips.enabled=true`, else stdlib |
| `internal/debug` | HTTP debug server on port 9900: pprof endpoints + `/debug/dump_config` |
| `internal/gounicorn` | Supervisor that wraps gunicorn (used inside the Python container) |
| `internal/outpost/ak` | `APIController` — REST + websocket to authentik server; healthcheck loop |
| `internal/outpost/ak/entrypoint` | `OutpostMain(name, ServerFactory)` — common bootstrap pattern |
| `internal/outpost/ldap` | LDAP server (bind handlers in `bind/`, search handlers in `search/`, group sync in `group/`, metrics in `metrics/`) |
| `internal/outpost/proxyv2` | HTTP forward-auth proxy (modern: v2 supersedes v1). Sessionstore backends: filesystem, postgres, sessionstore (default in-memory) |
| `internal/outpost/radius` | RADIUS server (PAP, CHAP, MS-CHAPv2; metrics) |
| `internal/outpost/rac` | RDP/SSH/VNC gateway (`connection/`) |
| `internal/outpost/flow` | Internal SDK for invoking flow execution from outposts |
| `internal/utils/{web,sentry,unix}` | HTTP transport with User-Agent, Sentry tracesampler, Unix socket helpers |
| `internal/web` | The lightweight router that fronts both Django and the embedded outpost; `BrandTLS` for per-brand SNI cert serving |

### Frontend modules (`web/src/`)

| Module | Purpose |
|---|---|
| `web/src/admin/` | Admin Console SPA — entrypoint `admin/index.entrypoint.ts`. Lazy-loaded route per resource. |
| `web/src/user/` | User self-service SPA — `user/index.entrypoint.ts`. Application library, settings, MFA management. |
| `web/src/flow/` | Flow executor SPA — `flow/index.entrypoint.ts`. Renders one stage at a time, receives next-stage JSON from server. |
| `web/src/rac/` | RAC client (Guacamole-protocol over WebSocket) |
| `web/src/standalone/api-browser/` | Embedded Swagger UI for `schema.yml` |
| `web/src/standalone/loading/` | First-paint splash before the entrypoint hydrates |
| `web/src/elements/` | Shared Lit web components: tables, dialogs, charts, dual-select, search-ql, ak-mdx, etc. |
| `web/src/common/` | API client glue (`api/`), error formatting (`errors/`), websocket (`ws/`), sentry init, helpers, locale, ui |
| `web/src/components/` | Higher-level composed components: ak-search-ql, ak-wizard, sync widget, notifications |
| `web/packages/sfe/` | **Simplified Flow Executor** — limited fallback for IE-11/MSEdge-18 (specifically Office 365/Teams). Supports only: identification, password, redirect, autosubmit, authenticator_validate (code + WebAuthn). See `web/packages/sfe/README.md`. |

### Build infrastructure

| Tooling | Where |
|---|---|
| Python packaging | `pyproject.toml` (uv-compatible) |
| Go module | `go.mod` (single module at repo root for all `cmd/*` and `internal/*`) |
| Rust workspace | `Cargo.toml` (workspace at repo root; members under `packages/ak-axum`, `packages/ak-common`, `packages/client-rust`, `website/scripts/docsmg`) |
| npm workspace | `package.json` with `workspaces: ["packages/*", "web/*", "website/*"]` |
| Bundler | esbuild (custom config in `web/bundler/`) — fast, simple, no webpack |
| Test runners | pytest (Python), `go test ./...` (Go), `cargo test` (Rust), Playwright (web e2e), `wtr` (web unit) |
| Lint/format | ruff + black (Python), `gofmt` + `staticcheck` (Go), `rustfmt` + clippy (Rust), eslint + prettier (TS/JS) |

---

## 5. Request Lifecycle

A typical OIDC login walks through every layer of authentik. Here's the journey of one request:

```text
  Browser                Reverse Proxy        authentik server          PostgreSQL
   ───────                ──────────────       ─────────────────        ──────────

  GET https://grafana → … redirects → GET /application/o/authorize/?client_id=…&redirect_uri=…
                                            │
                                            ▼
                           ┌──────────────────────────────────────────┐
                           │ internal/web/ router                     │
                           │  - reads SNI → BrandTLS                  │
                           │  - matches /application/o/ → Django      │
                           └─────────────────┬────────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Django + DRF (Python)                   │
                           │  authentik/providers/oauth2/views/      │
                           │     authorize.py: AuthorizationFlowInit │
                           │  - loads OAuth2Provider by client_id    │
                           │  - resolves bound Flow                  │
                           │  - kicks off FlowPlanner.plan()         │
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ authentik/flows/planner.py              │
                           │  FlowPlanner.plan(flow, request, ctx)   │
                           │  - cache lookup (django-postgres-cache) │
                           │  - if miss: walks FlowStageBindings,    │
                           │    evaluates PolicyBindings per stage   │
                           │  - returns FlowPlan(stages=[...])       │
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ FlowExecutorView.dispatch()             │
                           │  - pop next stage from plan             │
                           │  - render its template/JSON to client   │
                           └─────────────────┬───────────────────────┘
                                             ▼
  302 redirect to /if/flow/default-authentication-flow/...
                                             │
                                             ▼
  Browser loads /if/flow/... → web/src/flow/ SPA hydrates
                                             │
                                             ▼
  POST stage payload (username, password) → FlowExecutorView
                                             │
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Per-stage logic                         │
                           │ - identification: User.objects.filter() │
                           │ - password: verify_password()           │
                           │ - mfa_validate: WebAuthn challenge      │
                           │ - user_login: session = create_session()│
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Final stage: redirect back to provider  │
                           │ - issues authorization code             │
                           │ - 302 → grafana.example.com/...?code=…  │
                           └─────────────────────────────────────────┘
                                             │
                                             ▼
  Grafana → POST /application/o/token/?code=…
                                             │
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ TokenView: validate code, issue JWT     │
                           │  (signed with provider's CertificateKey)│
                           └─────────────────────────────────────────┘
                                             │
                                             ▼
  Grafana → GET /application/o/userinfo/ → 200 with claims
                                             │
                                             ▼
  Grafana session created → user lands on dashboard
```

**Key insight**: every stage transition is a fresh HTTP roundtrip with form POST. The flow's state lives server-side in the session — the client sees only the rendered HTML/JSON for one stage at a time. This is what makes flows safe to interrupt, customize, and policy-gate.

### Embedded outpost path

For Proxy provider requests:

```text
  Browser → GET https://app.example.com/protected
        → app's reverse proxy → forward-auth to authentik
        → :9000/outpost.goauthentik.io/auth/traefik
        │
        ▼
  internal/web/ router matches /outpost.goauthentik.io/ → embedded outpost
        │
        ▼
  internal/outpost/proxyv2 — check session, on miss start a flow
```

The embedded outpost shares process space with the server. When `AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true`, the router 404s those paths and a separate `authentik-proxy` outpost is required.

---

## 6. The Flow Engine

The Flow engine is the single most important piece of code in authentik. It's implemented in pure Python and is general enough to express **any** sequence of user interactions, from a 1-stage password-only login to a 12-stage MFA-enrolment-with-consent-and-email-verification flow.

### Model graph (`authentik/flows/models.py`)

```text
Flow ─── 1:N ─── FlowStageBinding ─── N:1 ─── Stage
  │                    │                       │
  │                    │                       └─ (one of 22 concrete subclasses)
  │                    │
  │                    └─ PolicyBinding (N:1 to Policy)
  │
  ├─ designation: enum {authentication, invalidation, recovery,
  │                     unenrollment, enrollment, stage_configuration}
  ├─ authentication: enum {none, require_authenticated, require_unauthenticated,
  │                         require_superuser, require_redirect, require_outpost}
  ├─ policy_engine_mode: enum {any, all}
  ├─ denied_action: enum {message_continue, message, continue}
  └─ layout: enum {stacked, content_left, content_right, sidebar_left, sidebar_right}

FlowStageBinding extends PolicyBindingModel:
  ├─ order: int                # sort key
  ├─ re_evaluate_policies: bool # re-check on every stage entry
  ├─ invalid_response_action: enum {retry, restart, restart_with_context}

Stage (abstract base):
  └─ verbose_name + 22 concrete subclasses each in authentik/stages/<n>/models.py
```

### The Planner (`authentik/flows/planner.py`)

```python
class FlowPlan:
    flow_pk: UUID                 # which flow this plan belongs to
    bindings: list[FlowStageBinding]
    markers: list[StageMarker]    # parallel array; each marker can ReevaluateMarker, SkipMarker
    context: dict                 # arbitrary key/value; survives across stages
```

The `FlowPlanner.plan(flow, request)` method:

1. **Cache check** — `cache_key(flow, request.user)` → lookup in `django-postgres-cache` (TTL: `cache.timeout_flows`, default 300 s). Cache hit returns a pre-computed FlowPlan; cache miss continues.
2. **Walk FlowStageBindings** ordered by `order`.
3. **For each binding**, evaluate the PolicyBindings attached:
   - If `policy_engine_mode = all`, all must pass; else `any` must pass.
   - If policies fail and the stage is marked `negate=True`, skip the stage.
4. **Build the FlowPlan**, store it in the cache, return it.
5. The `FlowExecutorView` pops bindings from the plan and renders them one at a time.

The cache_key includes `flow.pk + user.pk` (when user is known) so per-user policy decisions are remembered. The planner respects `re_evaluate_policies=True` on individual bindings — those skip the cache for that one stage.

### Why this is powerful

- **Adding MFA to a flow** = adding one FlowStageBinding to the existing Flow. No code change.
- **A/B-testing two stage types** = bind both with mutually-exclusive policies.
- **Conditional consent** = bind a Consent stage gated by an Expression policy that checks the requested scopes.
- **Multi-tenant differentiation** = each Brand chooses different Flows for the same purpose.

### Re-evaluation markers

Some stages mutate state mid-flow (e.g. user_write creates a user; afterwards group-membership policies need to be re-checked). Setting `re_evaluate_policies=True` on the *downstream* binding forces the planner to re-evaluate when the user re-enters that stage. The implementation is in `flows/markers.py` — `ReevaluateMarker` and `SkipMarker` are the two marker types tracked alongside the bindings list.

### Stage extension model

Every stage is a self-contained Django app under `authentik/stages/<name>/`:

```text
authentik/stages/password/
├── apps.py             # AuthentikStagePasswordConfig
├── models.py           # PasswordStage extends Stage
├── stage.py            # PasswordStageView extends ChallengeStageView
├── forms.py            # form schema if Django forms used
├── migrations/         # standard Django migrations
├── tests/              # pytest test cases
└── api/                # DRF ViewSet for /api/v3/stages/password/
```

To add a new stage type, create a new app following this layout, register it in `authentik/root/settings.py`, write the migration. The flow engine handles discovery via `apps.get_app_configs()` — no central registry.

---

## 7. Provider Implementations

Providers are protocol-specific outbound integrations. Each lives at `authentik/providers/<name>/` (or `authentik/enterprise/providers/<name>/` for EE).

### Common pattern

| File | Purpose |
|---|---|
| `models.py` | `XxxProvider` subclass of `Provider`. Adds protocol-specific fields. |
| `views/` | HTTP views (e.g. `authorize.py` for OAuth2, `sso.py` for SAML) |
| `api/` | DRF ViewSet for `/api/v3/providers/<name>/` |
| `signals.py` | Django signals (e.g. clear cache on provider edit) |
| `templates/` | Server-rendered HTML (mostly SAML for AssertionConsumerService POST) |
| `tests/` | Per-provider test coverage |
| `migrations/` | Schema evolution |

### OAuth2 (`authentik/providers/oauth2/`)

The most-used provider. Implements OAuth2 + OIDC + extensions (PKCE, device code grant, RP-initiated logout, dynamic client registration, token introspection, revocation).

| View | Path | Purpose |
|---|---|---|
| `AuthorizationFlowInitView` | `/application/o/authorize/` | Starts the flow for authorization_code, implicit, hybrid |
| `TokenView` | `/application/o/token/` | Exchanges code/refresh for tokens |
| `UserInfoView` | `/application/o/userinfo/` | Returns claims for an access token |
| `JWKSView` | `/application/o/<slug>/jwks/` | JSON Web Key Set for signature verification |
| `ProviderInfoView` | `/application/o/<slug>/.well-known/openid-configuration` | OIDC discovery |
| `EndSessionView` | `/application/o/<slug>/end-session/` | RP-initiated logout |
| `TokenRevokeView` | `/application/o/revoke/` | RFC 7009 revocation |
| `TokenIntrospectionView` | `/application/o/introspect/` | RFC 7662 introspection |
| `DeviceView` | `/application/o/device/` | RFC 8628 device code grant |
| `DynamicClientRegistrationView` | `/application/o/register/` | RFC 7591 |

**Token signing**: each provider has a `signing_key` FK to `CertificateKeyPair`. Tokens are signed via `jwcrypto` (RS256/ES256/EdDSA). The `extracted/class_hierarchy.tsv` shows 1483 classes — many are JWT payload variants for the different grant types.

**Token storage**: access tokens are stored in `authentik_providers_oauth2_accesstoken` with TTL fields. Refresh tokens in `authentik_providers_oauth2_refreshtoken`. Authorization codes are in `authentik_providers_oauth2_authorizationcode` with 60s TTL. Cleanup runs every minute via a scheduled task.

**Throttling**: configured in `default.yml`:

```yaml
throttle:
  providers:
    oauth2:
      device: 20/hour       # device-code-grant rate limit
  default: 1000/second
```

The throttle applies to the device-code endpoint specifically (the only one a non-authenticated client can hit at high rate).

### SAML (`authentik/providers/saml/`)

| File | Purpose |
|---|---|
| `processors/` | `request_parser.py`, `response_assertion.py`, `metadata.py` — XML handling |
| `views/sso.py` | Single Sign-On views (redirect + POST bindings) |
| `views/slo.py` | Single Logout views |
| `utils/encoder.py` | XMLSEC signing/encryption wrappers |

Uses `xmlsec` (libxmlsec1 C library binding) + `lxml`. The `defusedxml` package is in the dep tree to prevent XXE attacks (verified via `pyproject.toml`).

NameID formats supported: `persistent`, `transient`, `emailAddress`, `unspecified`, `windowsDomainQualifiedName`, `X509SubjectName`.

Binding support: HTTP-Redirect, HTTP-POST. Artifact binding is **not** supported. ECP profile is not supported. Both IdP-initiated and SP-initiated flows work.

### LDAP provider (`authentik/providers/ldap/`)

Unlike the other providers, LDAP runs in a Go outpost (`internal/outpost/ldap/`). The Python side is mostly metadata:

| Python side | Go side |
|---|---|
| Provider model: search/bind DNs, search group, MFA support, TLS settings | `bind/direct/`, `bind/memory/` handlers per auth method |
| `Controller` class (`controllers/ldap.py`) renders config for outpost | `search/direct/`, `search/memory/` for search ops |
| API for `/api/v3/providers/ldap/` | `group/` for memberOf resolution |
| | `metrics/` for Prometheus |

The outpost gets its config via WebSocket from the server (see `internal/outpost/ak/`).

### Other providers (briefer)

| Provider | Highlights |
|---|---|
| **RADIUS** (`authentik/providers/radius/`) | Goes through `internal/outpost/radius/`. Supports PAP, CHAP, MS-CHAPv2. Uses `pyrad` on Python side, native UDP on Go side. |
| **Proxy** (`authentik/providers/proxy/`) | Three modes: forward_single, forward_domain, proxy. Outpost in `internal/outpost/proxyv2/`. Session stores: filesystem, postgres, in-memory. |
| **RAC** (`authentik/providers/rac/`, EE) | Endpoints, ConnectionTokens. Go outpost speaks Guacamole protocol → translates to RDP/SSH/VNC. |
| **SCIM** (`authentik/providers/scim/`) | Outbound user/group provisioning. `clients/users.py`, `clients/groups.py`, `clients/schema.py`. Uses `scim2-filter-parser` for compatibility. |
| **MS Entra** (`authentik/enterprise/providers/microsoft_entra/`, EE) | Uses `msgraph-sdk`. Periodic full + delta sync. |
| **Google Workspace** (`authentik/enterprise/providers/google_workspace/`, EE) | Uses `google-api-python-client`. Same pattern. |
| **WS-Federation** (`authentik/enterprise/providers/ws_federation/`, EE) | Legacy SharePoint and old .NET. Generates `wresult` SAML tokens. |
| **SSF** (`authentik/enterprise/providers/ssf/`, EE) | Shared Signals Framework — pushes events outbound to SaaS that consumes SSF. |

---

## 8. Outpost Architecture

Outposts solve a fundamental problem: Django, even on uvicorn, can't economically handle raw-protocol LDAP/RADIUS/RDP connections. So authentik splits these into separate Go processes that own the wire protocol and talk to the Python server via REST + WebSocket for configuration.

### Common bootstrap (`internal/outpost/ak/entrypoint/`)

Every outpost binary uses the same `OutpostMain()` helper:

```go
// cmd/proxy/main.go
RunE: func(cmd *cobra.Command, args []string) error {
    err := entrypoint.OutpostMain("authentik.outpost.proxy", proxyv2.NewProxyServer)
    if err != nil {
        fmt.Println(helpMessage)
    }
    return err
}
```

`OutpostMain` does:

1. Read `AUTHENTIK_HOST`, `AUTHENTIK_TOKEN`, `AUTHENTIK_INSECURE` from env.
2. Build an `APIController` (`internal/outpost/ak/api.go`) — makes a REST call to `/outposts/instances/?token=...` to register, opens a WebSocket to `/ws/outpost/<uuid>/` for config updates.
3. Invoke the server factory (e.g. `proxyv2.NewProxyServer`) with the controller as the config source.
4. Wait for an exit signal; on receipt, gracefully shut down all listeners.

### Required env vars

| Var | Required | Default | Purpose |
|---|---|---|---|
| `AUTHENTIK_HOST` | Yes | — | Where to reach authentik (e.g. `https://authentik.example.com`) |
| `AUTHENTIK_TOKEN` | Yes | — | Outpost auth token (generated when outpost is created in UI) |
| `AUTHENTIK_INSECURE` | No | `false` | Skip TLS verification (for self-signed certs in lab) |
| `AUTHENTIK_HOST_BROWSER` | No | = `AUTHENTIK_HOST` | URL to send to browsers (when outpost is on a private network) |
| `AUTHENTIK_LOG_LEVEL` | No | `info` | Log level |

The proxy outpost's `helpMessage` documents these inline (`cmd/proxy/main.go:18-28`).

### Healthcheck protocol

All outposts share a common healthcheck CLI: `<binary> healthcheck <mode>`. Modes:

| Mode | Checks |
|---|---|
| `allinone` / `server` | `http://localhost<path>-/health/live/` returns 200 |
| `worker` | `http://localhost/-/health/live/` |

Used in `Dockerfile` as the `HEALTHCHECK` instruction. Returns exit code 0 (healthy) or 1.

### Wire protocol — outpost ↔ server

Outposts speak two channels:

1. **REST** — periodic state polling (`/api/v3/outposts/instances/<id>/`) and ad-hoc reads (provider config, user attributes for the embedded session lookup).
2. **WebSocket** — `/ws/outpost/<uuid>/` with JSON messages:

| Message type | Direction | Payload |
|---|---|---|
| `hello` | outpost → server | version, build hash, hostname |
| `ack` | server → outpost | acknowledgement of hello |
| `triggerUpdate` | server → outpost | "config changed, refresh" |
| `providerSpecific` | both | per-provider data (e.g. LDAP user lookup, proxy session validation) |

The websocket reconnects with exponential backoff on disconnect. If the server is unreachable for >5 minutes, the outpost continues serving with its last-known config (degraded mode).

### Proxy outpost specifics (`internal/outpost/proxyv2/`)

Sub-packages:

| Package | Purpose |
|---|---|
| `application/` | Per-application `RoutingApp` instance |
| `codecs/` | HS256 signing for forward-auth signed redirects |
| `filesystemstore/`, `postgresstore/`, `sessionstore/` | Session storage backends (default: in-memory `sessionstore`) |
| `metrics/` | Prometheus metrics |
| `templates/` | Error pages |
| `types/` | Wire types |
| `hs256/` | HMAC-SHA256 helpers |

The proxy outpost terminates the application's URL, decides if the request has a valid session, and either forwards (with injected headers) or redirects to the authentik flow.

### LDAP outpost specifics (`internal/outpost/ldap/`)

Built on `github.com/jimlambrt/gldap`. Sub-packages:

| Package | Purpose |
|---|---|
| `bind/direct/`, `bind/memory/` | Two bind strategies: direct (query authentik on every bind) and memory (cache for performance, with TTL) |
| `search/direct/`, `search/memory/` | Same split for search ops |
| `server/` | Net listener and connection handler |
| `group/` | Group resolution + memberOf computation |
| `flags/`, `constants/`, `utils/`, `handler/` | Plumbing |
| `metrics/` | Prometheus |

The memory backend caches users/groups in-process; the direct backend hits authentik on every operation. Memory is faster but uses RAM proportional to your directory size. Default is configurable per provider.

### RADIUS outpost (`internal/outpost/radius/`)

Stateless. Each Access-Request triggers a flow execution synchronously. The flow runs to completion server-side; outpost returns Access-Accept or Access-Reject. MS-CHAPv2 is implemented; PAP/CHAP fall back to that.

### RAC outpost (`internal/outpost/rac/`)

Hosts a WebSocket → Guacamole protocol bridge. The web client at `web/src/rac/` connects via WebSocket; the outpost translates between Guacamole messages and the configured RDP/SSH/VNC endpoint. Connections short-lived; each new session re-validates with the server.

---

## 9. The Blueprint System

Blueprints are authentik's GitOps interface. Every object in the API can be expressed as YAML and applied declaratively. The implementation lives in `authentik/blueprints/`.

### Loader (`authentik/blueprints/v1/`)

| Module | Purpose |
|---|---|
| `common.py` | YAML loader registration with custom tags |
| `importer.py` | Walks `entries` list, instantiates models, persists |
| `exporter.py` | Inverse: dumps models to YAML preserving FK relationships |
| `meta/` | Self-describing metadata for the blueprint format itself |

### YAML tags

The loader registers a set of `!Tag` constructors that get evaluated at apply time:

| Tag | Implementation | Use |
|---|---|---|
| `!Env VAR_NAME` | Reads `os.environ[VAR_NAME]` | Inject secrets from env into blueprints |
| `!Find [model, [field, value]]` | Calls `Model.objects.get(field=value)` | Reference existing objects by lookup |
| `!KeyOf id_in_blueprint` | Cross-references `identifiers.id` within the same blueprint | Wire blueprint-local FKs |
| `!Format [template, *args]` | Python `.format()` | String building |
| `!If [condition, then, else]` | Conditional | Dynamic blueprints |
| `!Context key` | Reads from `BlueprintInstance.context` | Apply-time variable injection |

### Apply flow

```text
File watcher (authentik/blueprints/apps.py)
     │ on file create/modify
     ▼
Importer (v1/importer.py)
     │ parse YAML
     │ for each entry:
     │   - resolve identifiers → existing model? create_or_update
     │   - resolve attrs (eval tags)
     │   - validate via DRF serializer
     │   - save in transaction
     ▼
BlueprintInstance row updated with last_applied_hash
     │
     ▼
Emit Event(action="blueprint_apply")
```

### Discovery

The `apps.py` registers two file watchers via `watchdog`:

1. `/blueprints/` (mount point, configurable via `AUTHENTIK_BLUEPRINTS_DIR`)
2. The bundled `blueprints/default/` and `blueprints/system/`

On change, the file is hashed and compared to `BlueprintInstance.last_applied_hash` in PostgreSQL. If different, re-apply. This means: change a blueprint, the worker picks it up within seconds and re-applies it — no restart needed.

### Bundled blueprints

| Directory | Purpose |
|---|---|
| `blueprints/default/` | The default flows, stages, policies that ship with authentik. Re-applied on upgrade to recover from accidental deletions. |
| `blueprints/example/` | Reference blueprints — provider configs for Slack, Nextcloud, AWS, GitLab, etc. Not auto-applied. |
| `blueprints/system/` | Internal blueprints used by authentik itself (e.g. system-event configuration). |
| `blueprints/testing/` | CI-only blueprints used to seed test fixtures. |
| `blueprints/migrations/` | Schema evolution blueprints (when a Django migration alone can't fully express the change). |

### Apply via CLI

`authentik/blueprints/management/commands/apply_blueprint.py:36`:

```bash
docker compose exec worker ak apply_blueprint /path/to/file.yaml
docker compose exec worker ak apply_blueprint --export > all-current-state.yaml
```

The `--export` is the inverse: dumps the current DB state as a blueprint, useful for backing up a working configuration.

---

## 10. Configuration Reference

`authentik/lib/default.yml` is the canonical baseline. Every key is overridable via:

1. Mounted `local.env.yml` (merged with `default.yml` via deep-merge — uses the `deepmerge` package).
2. `AUTHENTIK_*` environment variables with `__` separating nested keys.

### Full keyspace (from `authentik/lib/default.yml`)

```yaml
postgresql:
  host: localhost            # AUTHENTIK_POSTGRESQL__HOST
  name: authentik
  user: authentik
  port: 5432
  password: "env://POSTGRES_PASSWORD"   # special: read from env at runtime
  sslmode: disable           # disable | allow | prefer | require | verify-ca | verify-full
  conn_max_age: 0            # seconds; 0 = no persistent
  conn_health_checks: false
  use_pool: false
  test: { name: test_authentik }
  default_schema: public
  read_replicas: {}          # 0: { host: ..., port: ... }, 1: { ... }

listen:
  http:    ["[::]:9000"]
  https:   ["[::]:9443"]
  ldap:    ["[::]:3389"]
  ldaps:   ["[::]:6636"]
  radius:  ["[::]:1812"]
  metrics: ["[::]:9300"]
  debug:    0.0.0.0:9900
  debug_py: 0.0.0.0:9901
  debug_tokio: "[::]:6669"
  trusted_proxy_cidrs:
    - 127.0.0.0/8
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
    - fe80::/10
    - ::1/128

http_timeout: 30

cache:
  # url: ""              # (default = django-postgres-cache; set to redis://… to use external)
  timeout: 300           # general cache TTL
  timeout_flows: 300     # FlowPlanner cache TTL
  timeout_policies: 300  # policy decision cache TTL

# channel:
#   url: ""              # (default = django-channels-postgres; set to redis:// to use external)

debug: false
debugger: false

log_level: info          # debug | info | warning | error | critical
log:
  http_headers:
    - User-Agent         # which incoming headers to log
  rust_log:              # per-module log levels for Rust frontend (when running src/)
    console_subscriber: info
    h2: info
    hyper_util: warn
    mio: info
    notify: info
    reqwest: info
    runtime: info
    rustls: info
    sqlx: info
    sqlx_postgres: info
    tokio: info
    tungstenite: info

sessions:
  unauthenticated_age: days=1

error_reporting:
  enabled: false
  sentry_dsn: https://[email removed]/...
  environment: customer
  send_pii: false
  sample_rate: 0.1

email:
  host: localhost
  port: 25
  username: ""
  password: ""
  use_tls: false
  use_ssl: false
  timeout: 10
  from: authentik@localhost
  template_dir: /templates

throttle:
  providers:
    oauth2:
      device: 20/hour
  default: 1000/second

outposts:
  container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
  discover: true
  disable_embedded_outpost: false

ldap:
  task_timeout_hours: 2
  page_size: 50
  tls: { ciphers: null }

sources:
  kerberos: { task_timeout_hours: 2 }

reputation:
  expiry: 86400          # seconds; reputation score lifetime

cookie_domain: null
disable_update_check: false
disable_startup_analytics: false

events:
  context_processors:
    geoip: "/geoip/GeoLite2-City.mmdb"
    asn: "/geoip/GeoLite2-ASN.mmdb"

compliance:
  fips:
    enabled: false       # set true → FIPS-only crypto

cert_discovery_dir: /certs

tenants:
  enabled: false         # django-tenants (schema-per-tenant); separate from Brands
  api_key: ""

blueprints_dir: /blueprints

web:
  workers: 2             # gunicorn worker processes
  threads: 4             # threads per worker
  path: /                # URL path prefix
  timeout_http_read_header: 5s
  timeout_http_read: 30s
  timeout_http_write: 60s
  timeout_http_idle: 120s

worker:
  processes: 1           # dramatiq worker processes
  threads: 2             # threads per process
  consumer_listen_timeout: seconds=30
  task_max_retries: 5
  task_default_time_limit: minutes=10
  task_purge_interval: days=1
  task_expiration: days=30
  scheduler_interval: seconds=60

storage:
  backend: file          # file | s3
  file:
    path: ./data
    url_expiry: minutes=15
  s3:
    # region: us-east-1
    # use_ssl: true
    # endpoint: https://s3.us-east-1.amazonaws.com
    # signature_version: s3v4
    # access_key: ""
    # secret_key: ""
    # bucket_name: authentik-data
    # custom_domain: null
    secure_urls: true
    url_expiry: minutes=15

skip_migrations: false
```

### Env-var convention

Two-level nesting: `AUTHENTIK_<TOP>__<SUB>=value`. Three-level: `AUTHENTIK_<TOP>__<SUB>__<SUB2>=value`. Examples:

```bash
AUTHENTIK_POSTGRESQL__HOST=db.example.com
AUTHENTIK_POSTGRESQL__SSLMODE=require
AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__HOST=replica1.example.com
AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__PORT=5432
AUTHENTIK_EMAIL__USE_TLS=true
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=internal.registry.example.com/authentik/%(type)s:%(version)s
AUTHENTIK_LOG__RUST_LOG__SQLX=warn
```

The parser is in `authentik/lib/config.py` (`AttrEncoder`, `set_path_in_dict`). The Go side has a twin at `internal/config/config.go` reading the same YAML.

### Build-time env vars (`extracted/config_keys.tsv`)

These are read at container build / startup, not runtime config:

| Var | Used by | Purpose |
|---|---|---|
| `POSTGRES_PASSWORD` | `default.yml` `env://` substitution | Inject DB password without writing it to a config file |
| `WORKSPACE_DIR` | `packages/ak-common/src/config/mod.rs:33` | Rust workspace root (build-time) |
| `AUTHENTIK_ENV` | `packages/ak-common/src/config/mod.rs:41` | `dev` / `production` switch for Rust frontend |
| `NODE_ENV` | esbuild | `production` enables minification |
| `AK_LOG_LEVEL` | `web/logger/node.js:38` | Build-time log level |
| `AK_BUNDLER` | `web/bundler/utils/node.js:30` | Pick bundler (`esbuild` default) |
| `AK_API_BASE_PATH` | `web/bundler/utils/node.js:29` | API base path injected into JS bundle |
| `AK_DOCUSAURUS_ENV` | `packages/docusaurus-config/lib/common.js:46` | Docs build environment |
| `GIT_BUILD_HASH` | `internal/constants/constants.go:13` | Embedded in Go binaries' version string |
| `dramatiq_prom_host` / `dramatiq_prom_port` | `packages/django-dramatiq-postgres/django_dramatiq_postgres/forks.py:14-15` | Dramatiq Prometheus exporter bind |

---

## 11. Database Schema and Migrations

### Schema overview

Major tables (Django app prefix in parentheses):

| Table | App | Purpose |
|---|---|---|
| `core_user` | `authentik_core` | Users (extends `django.contrib.auth.models.AbstractUser`) |
| `core_group` | `authentik_core` | Groups |
| `core_application` | `authentik_core` | Applications |
| `core_source` | `authentik_core` | Sources (abstract base) |
| `core_token` | `authentik_core` | API tokens + recovery tokens |
| `core_authenticatedsession` | `authentik_core` | Active sessions |
| `core_propertymapping` | `authentik_core` | Property mappings (abstract base) |
| `flows_flow` | `authentik_flows` | Flows |
| `flows_stage` | `authentik_flows` | Stages (abstract base) |
| `flows_flowstagebinding` | `authentik_flows` | Stage bindings |
| `flows_flowtoken` | `authentik_flows` | Continuation tokens |
| `policies_policy` | `authentik_policies` | Policies (abstract base) |
| `policies_policybinding` | `authentik_policies` | Polymorphic bindings (Flow ↔ Policy, Stage ↔ Policy, App ↔ Policy) |
| `providers_oauth2_oauth2provider` | `authentik_providers_oauth2` | OAuth2 providers |
| `providers_oauth2_accesstoken` | `authentik_providers_oauth2` | Access tokens |
| `providers_oauth2_refreshtoken` | `authentik_providers_oauth2` | Refresh tokens |
| `providers_oauth2_authorizationcode` | `authentik_providers_oauth2` | Auth codes (60s TTL) |
| `providers_saml_samlprovider` | `authentik_providers_saml` | SAML providers |
| (similar per provider/source/stage…) | | |
| `events_event` | `authentik_events` | Audit log |
| `events_notification` | `authentik_events` | Notifications |
| `events_notificationrule` | `authentik_events` | Notification rules |
| `events_notificationtransport` | `authentik_events` | Notification transports |
| `outposts_outpost` | `authentik_outposts` | Outpost instances |
| `outposts_outpostserviceconnection` | `authentik_outposts` | Docker/K8s connection configs |
| `crypto_certificatekeypair` | `authentik_crypto` | TLS certificates and keys |
| `blueprints_blueprintinstance` | `authentik_blueprints` | Applied blueprints (with hash) |
| `brands_brand` | `authentik_brands` | Multi-tenant brands |
| `tenants_tenant` | `authentik_tenants` | django-tenants tenants (if enabled) |
| `rbac_role` | `authentik_rbac` | RBAC roles |
| `tasks_taskschedule` | `authentik_tasks` | Cron-like task schedules |

### Polymorphic models

Several abstract bases use `django-pgtrigger` to enforce that subclass tables share a UUID with the parent table:

- `Stage` → `IdentificationStage`, `PasswordStage`, …
- `Provider` → `OAuth2Provider`, `SAMLProvider`, …
- `Source` → `LDAPSource`, `OAuthSource`, …
- `Policy` → `ExpressionPolicy`, `EventMatcherPolicy`, …
- `PropertyMapping` → `OAuth2ScopeMapping`, `SAMLPropertyMapping`, …

`SerializerModel` (in `authentik/lib/models.py`) is a mixin that asserts every model has an associated DRF Serializer for the API.

### Migrations

| Where | Purpose |
|---|---|
| `authentik/*/migrations/*.py` | Standard Django migrations (auto-generated + hand-written) |
| `blueprints/migrations/` | Schema-evolution blueprints (when YAML changes are needed alongside Python migrations) |
| `lifecycle/system_migrations/` | Imperative one-shot scripts run on container start (e.g. fix a corrupted index) |

Migrations are run in the worker container at startup via `lifecycle/migrate.py`:

```python
# pseudocode
wait_for_db()           # lifecycle/wait_for_db.py
manage.migrate()        # standard Django
run_system_migrations() # lifecycle/system_migrations/
apply_default_blueprints()
```

> [!WARNING]
> Migrations are **forward-only**. Skipping minor releases can fail (see [#22688](https://github.com/goauthentik/authentik/issues/22688), upgrade `2025.10.2 → 2026.5.0` broke migration `authentik_core.0058_setup`). The supported path is to upgrade through each minor in sequence.

### Database performance considerations

- The default `conn_max_age: 0` opens a new PostgreSQL connection per request. Set to `60` for ~10× lower DB CPU at the cost of more idle connections. Set to `-1` for "persistent until explicit close".
- Set `use_pool: true` to enable the `psycopg[pool]` connection pool. Recommended for production at scale.
- Read replicas are honored automatically for ViewSet `list` and `retrieve` actions (when `read_replicas` is configured).

---

## 12. Background Workers and Scheduling

### Architecture

```text
                 ┌─────────────────────┐
                 │     PostgreSQL      │
                 │  ┌───────────────┐  │
                 │  │ dramatiq_     │  │
                 │  │  postgres_    │  │
                 │  │  queue        │  │
                 │  │  (FIFO table) │  │
                 │  └───────┬───────┘  │
                 └──────────┼──────────┘
                            │ LISTEN/NOTIFY
                            ▼
        ┌───────────────────────────────────────┐
        │   worker container                     │
        │   ┌──────────────────────────────┐     │
        │   │ Scheduler (single instance)   │     │
        │   │  - reads tasks_taskschedule   │     │
        │   │  - enqueues at scheduled time │     │
        │   └──────────────────────────────┘     │
        │   ┌──────────────────────────────┐     │
        │   │ Worker processes (N)          │     │
        │   │  - each runs dramatiq         │     │
        │   │  - pulls from queue           │     │
        │   │  - runs task → result/error   │     │
        │   └──────────────────────────────┘     │
        └───────────────────────────────────────┘
```

### Why dramatiq?

`dramatiq` was chosen over Celery for cleaner middleware, better retry semantics, and a simpler API. The PostgreSQL broker (`packages/django-dramatiq-postgres/`) was custom-built to eliminate the Redis dependency.

### Task discovery

Each Django app can define tasks in `<app>/tasks.py`. The `authentik/tasks/apps.py` auto-discovers them via `apps.get_app_configs()` on startup. Example tasks:

- `authentik.providers.scim.tasks.scim_sync_all` — periodic SCIM sync
- `authentik.events.tasks.event_notification_handler` — dispatch notifications
- `authentik.events.tasks.notification_cleanup` — purge old notifications
- `authentik.outposts.tasks.outpost_controller` — reconcile outposts with Docker/K8s state
- `authentik.sources.ldap.tasks.ldap_sync_all` — periodic LDAP sync per source
- `authentik.crypto.tasks.certificate_discovery` — watch `/certs/` for new files

### Schedules

`TaskSchedule` rows in PostgreSQL define cron-like schedules. The scheduler (single-instance) polls every `worker.scheduler_interval` (default 60s) and enqueues due tasks. Worker processes pick them up.

### Retry semantics

From `default.yml`:

```yaml
worker:
  task_max_retries: 5
  task_default_time_limit: minutes=10
  task_purge_interval: days=1
  task_expiration: days=30
```

Failed tasks retry with exponential backoff up to 5 times, with a hard 10-minute limit per attempt. Completed/failed tasks are purged daily; the task history is kept for 30 days.

### Pre-flight

`lifecycle/wait_for_db.py` blocks worker startup until PostgreSQL is ready. After DB is up, `lifecycle/migrate.py` runs migrations, then dramatiq starts.

> [!IMPORTANT]
> Worker startup can fail if migrations themselves fail (e.g. lock timeout, corrupt schema). If you see "worker not starting", check the worker logs for `migrat` keywords. The server can run without the worker (degraded mode — no background tasks fire), but the worker requires a healthy DB.

---

## 13. API Surface (`/api/v3/`)

The REST API is generated by `drf-spectacular` from DRF ViewSets across all Django apps. The OpenAPI spec is at `schema.yml` and rendered as Swagger UI at `/api/v3/swagger/`.

### Versioning

The current version is `v3` (path-versioned). Earlier `v2` and `v1` are retired. A future `v4` would live at `/api/v4/`; both could coexist.

### Authentication

Three methods, by header:

| Method | Header | Use |
|---|---|---|
| Bearer token | `Authorization: Bearer <token>` | API-key access (user-scoped) |
| Session cookie | (browser default) | Admin UI calls |
| Outpost token | `Authorization: Bearer <token>` | Outpost ↔ server (the token scope is per-outpost, not user) |

API tokens are generated under **Admin → Directory → Tokens** or via the API itself. Service-account users hold tokens with no associated interactive login.

### Major endpoint groups

| Path | Resource |
|---|---|
| `/api/v3/core/users/` | User CRUD + impersonation, password set, MFA management |
| `/api/v3/core/groups/` | Group CRUD + membership |
| `/api/v3/core/applications/` | Application CRUD |
| `/api/v3/core/sources/` | Source listing (polymorphic) |
| `/api/v3/flows/instances/` | Flow CRUD |
| `/api/v3/flows/bindings/` | FlowStageBinding CRUD |
| `/api/v3/flows/executor/<slug>/` | Execute a flow programmatically |
| `/api/v3/stages/<type>/` | One ViewSet per stage type |
| `/api/v3/providers/<type>/` | One ViewSet per provider type |
| `/api/v3/sources/<type>/` | One ViewSet per source type |
| `/api/v3/policies/<type>/` | One ViewSet per policy type |
| `/api/v3/policies/bindings/` | PolicyBinding CRUD |
| `/api/v3/outposts/instances/` | Outpost CRUD |
| `/api/v3/outposts/service_connections/<type>/` | Docker / K8s connection configs |
| `/api/v3/crypto/certificatekeypairs/` | Cert CRUD + auto-generate + import |
| `/api/v3/events/events/` | Event read (write is internal) |
| `/api/v3/events/notifications/` | Notification CRUD |
| `/api/v3/events/transports/` | Transport CRUD |
| `/api/v3/events/rules/` | NotificationRule CRUD |
| `/api/v3/managed/blueprints/` | Blueprint upload + apply + export |
| `/api/v3/admin/system/` | System info, version, workers, settings |
| `/api/v3/rbac/roles/` | Role CRUD |
| `/api/v3/rbac/permissions/` | Permission listing |
| `/api/v3/tenants/tenants/` | Tenant CRUD (when `tenants.enabled=true`) |
| `/api/v3/tenants/settings/` | Tenant-scoped settings |

### Schema generation

```bash
docker compose exec worker ak build_schema --api-file schema.yml --blueprint-file blueprints/schema.json
```

Output: `schema.yml` (~3 MB, full OpenAPI 3.0) + `blueprints/schema.json` (JSON Schema for blueprint validation).

### Generated SDKs

| SDK | Path | Notes |
|---|---|---|
| TypeScript | `packages/client-ts/` | Used by `web/`. Auto-generated, do not edit. |
| Go | `packages/client-go/` | Used by outposts. |
| Rust | `packages/client-rust/` | Used by Rust frontend (`src/`). |

All three are regenerated via `openapi-generator-cli` on schema change. The generation is hooked into CI; manual regeneration is via `make gen-client-*` targets in the Makefile.

### Filtering, pagination, search

Standard DRF semantics: `?page=2`, `?page_size=50`, `?ordering=-created`, `?search=foo`, plus per-resource `?<field>=value` filters via `django-filter` (declared in each ViewSet).

DjangoQL (`djangoql` package) is exposed as a `?q=…` query on some endpoints — a more SQL-like search syntax. The `ak-search-ql` web component (`web/src/components/ak-search-ql/`) provides syntax highlighting + autocomplete.

---

## 14. Frontend Architecture (`web/`)

### Stack

| Layer | Choice |
|---|---|
| Framework | **Lit** (web components, ~6 KB runtime) |
| State | Lit reactive controllers + URL state |
| Styling | PatternFly 4 (server-hosted CSS) + custom theme |
| HTTP | Generated `@goauthentik/api` client (fetch wrapper) |
| Routing | `web/paths/` — minimal hash router |
| Bundler | esbuild (`web/bundler/`) |
| i18n | `@lit/localize` — XLF files in `web/xliff/`, dictionaries in `locale/<lang>/dictionaries/` |
| Editor | Monaco (lazy-loaded for blueprint editor) |
| Charts | Chart.js wrappers in `web/src/elements/charts/` |
| Testing | `@web/test-runner` (unit), `@playwright/test` (e2e in `web/e2e/`) |

### Entrypoint inventory

| Entrypoint | URL | Bundle |
|---|---|---|
| `admin/index.entrypoint.ts` | `/if/admin/` | Admin interface |
| `user/index.entrypoint.ts` | `/if/user/` | User interface |
| `flow/index.entrypoint.ts` | `/if/flow/<slug>/` | Flow executor |
| `rac/index.entrypoint.ts` | `/if/rac/<token>/` | RAC client |
| `standalone/api-browser/index.entrypoint.ts` | `/if/api-browser/` | Embedded Swagger UI |
| `standalone/loading/index.entrypoint.ts` | (initial paint) | Splash before route resolution |

### The Simplified Flow Executor (SFE)

`web/packages/sfe/` is a separate, intentionally minimal bundle for IE-11 / MSEdge-18 (Office 365 / Teams). Per `web/packages/sfe/README.md`:

> The Simplified Flow Executor is a limited fallback browser-side interpreter, written in TypeScript, for authentik's Flow language … It exists primarily to support late versions of Microsoft Office365 and Microsoft Teams, older software that still uses the MSEdge-18 and IE-11 Trident web engine.

Supports only 5 stages: identification, password, redirect, autosubmit, authenticator_validate. Other stage types return an error in SFE.

Built with Rollup (not esbuild) to fit the IE-11 module format. Pulls in:
- `core-js@3.49.0` for ES modern stdlib
- `formdata-polyfill`
- `weakmap-polyfill`
- `@webcomponents/template`

The detection logic that routes a Trident UA to the SFE bundle lives in `authentik/flows/views/executor.py` (server-side UA sniff).

### State management pattern

There's no global store (no Redux). Each route is its own Lit element with its own reactive controllers. Server state is the source of truth; on mutation, refetch. The `AKBaseElement` mixin (`web/src/elements/Base.ts`) provides locale + theme + global config.

### Locale

`locale/<lang>/LC_MESSAGES/*.po` files are compiled to `.xlf` for `@lit/localize`. Adding a language:

1. Add `<lang>` to `web/scripts/build-locales.mjs`.
2. Run `npm run extract-locales` to seed.
3. Translate.
4. Commit.

Server-side Django translations use the same `.po` files (django-translatable).

---

## 15. Operations Runbooks

### Daily checks

1. **Dashboard** → All systems healthy, worker count ≥ 1, no failed system tasks.
2. **System → Tasks** → No tasks stuck in "Running" for > 1 hour.
3. **Events** → Filter by severity = `warning` or `alert`; review.
4. **Outposts** → All Last seen < 5 minutes ago.

### Weekly checks

1. Backup PostgreSQL + `/data/` volume.
2. Check **System → Sources** sync results; investigate failures.
3. **Directory → Tokens** — review and prune unused tokens.
4. **Customization → Certificates** — note expiry dates of cert pairs in use.

### Adding a node to a Kubernetes HA install

```bash
kubectl scale deployment authentik-server --replicas=3
kubectl scale deployment authentik-worker --replicas=2
```

Server is stateless (except for the embedded outpost — set `outposts.disable_embedded_outpost=true` in HA and use external proxy outposts). Worker shouldn't be scaled past 1-2 unless your task load demands it; the scheduler runs in only one of them (leader-elected via PostgreSQL advisory lock).

### Rotating the secret key

```bash
NEW_KEY=$(openssl rand -base64 60 | tr -d '\n')

# 1. Set new key alongside old (comma-separated; first is used for signing, rest for verification)
AUTHENTIK_SECRET_KEY="${NEW_KEY},${OLD_KEY}" docker compose up -d

# 2. Wait for all sessions/tokens to expire (or revoke them)
# 3. Remove old key:
AUTHENTIK_SECRET_KEY="${NEW_KEY}" docker compose up -d
```

### Migrating from bundled to external PostgreSQL

```bash
# 1. Backup
docker compose exec postgresql pg_dump -U authentik authentik > backup.sql

# 2. Restore to external DB
psql -h db.example.com -U authentik -d authentik < backup.sql

# 3. Update .env
AUTHENTIK_POSTGRESQL__HOST=db.example.com
AUTHENTIK_POSTGRESQL__PASSWORD=...

# 4. Edit compose.yml — remove postgresql service
docker compose up -d
```

### Importing a TLS cert pair

```bash
docker compose exec worker ak import_certificate \
  --name "wildcard-example-com" \
  --certificate /certs/wildcard.crt \
  --private-key /certs/wildcard.key
```

Or drop them into `/certs/` (mounted into the worker) — they're auto-discovered by the `certificate_discovery` task.

### Tracing a specific user's login

1. **Events → Filter by user = `[email removed]`** → see chronological events.
2. **System → Logs** (if `log_level: debug`) for the worker — search for the user's email or session ID.
3. Server logs: `docker compose logs server | grep "session=alice"`.

### Recovering a locked-out admin

```bash
docker compose exec worker ak create_recovery_key 60 akadmin
```

Token URL is printed; navigate to it to bypass auth and reset.

### Disabling the embedded outpost in production

```bash
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true docker compose up -d
```

Then create a separate proxy outpost. This is the recommended config for HA installs (the embedded outpost can't coordinate across replicas; multiple separate outposts can).

### Force-reapply default blueprints

If you accidentally deleted a default flow:

```bash
docker compose exec worker ak apply_blueprint default/flow-default-authentication.yaml
```

Or set `AUTHENTIK_BLUEPRINTS__INTERVAL=10` to re-scan every 10 seconds (default is on change).

---

## 16. Security Analysis

### Threat model

| Asset | Threats | Mitigations |
|---|---|---|
| User credentials | Phishing, credential stuffing, brute force | MFA stages (TOTP/WebAuthn/Duo/etc.), reputation policy, captcha, GeoIP policy |
| Session cookies | Theft via XSS, CSRF | HttpOnly, Secure, SameSite=Lax; CSP headers; CSRF tokens on POST |
| API tokens | Leak, replay | Token scoping (RBAC), expiry, revocation, audit |
| Outpost ↔ server channel | MITM | TLS by default (set `AUTHENTIK_INSECURE=true` only for dev) |
| Database | Exposure of password hashes (argon2id) | Hash with `argon2-cffi`; reasonable cost parameters by default |
| SAML XML | XXE, signature wrapping | `defusedxml`, xmlsec signature validation |
| Blueprint apply | Privilege escalation via crafted blueprint | Restricted to admins or the file watcher (filesystem-mounted) |

### Authentication and authorization

- **Local password** — argon2id (`argon2-cffi==25.1.0`). Configurable cost in policy.
- **MFA** — TOTP (RFC 6238), WebAuthn (FIDO2, via `fido2==2.2.0`), Duo, SMS, email OTP.
- **Federated** — SAML/OAuth/LDAP/Kerberos/SCIM/Plex/Telegram.
- **Cert-based** — MTLS stage (EE) validates client X.509 cert against a configured trust chain.

### Cryptography

- TLS: stdlib OpenSSL by default; AWS-LC-RS with FIPS feature when `compliance.fips.enabled=true`.
- Signing: `cryptography==48.0.0` for X.509 ops, `jwcrypto==1.5.7` for JWS/JWE, `xmlsec==1.3.17` for SAML.
- Hashing: argon2id for passwords, SHA-256 for token fingerprints.
- Random: `secrets` (Python) for tokens, `crypto/rand` (Go) for outposts.

### CSRF protection

Django's default CSRF middleware is enabled. The Flow executor POSTs include CSRF tokens. API endpoints called from browser sessions also enforce CSRF; token-authenticated requests skip CSRF (the token itself is the auth).

### Audit trail

Every action emits an Event row:

- Authentication: `login`, `login_failed`, `logout`
- User mgmt: `user_write`, `user_delete`, `password_set`, `email_sent`
- Configuration: `model_created`, `model_updated`, `model_deleted`, `secret_view`
- Impersonation: `impersonation_started`, `impersonation_ended` (with both users recorded)
- Source: `source_linked`, `source_unlinked`
- Flow: `flow_execution` (records every flow run)
- Policy: `policy_execution` (only when configured to log)
- System: `update_available`, `configuration_error`

Events are stored in `authentik_events_event` indefinitely by default. Configure retention via a scheduled task or the EE audit log module.

### Known security advisories

`SECURITY.md` references the CVE disclosure process. Recent advisories at [docs.goauthentik.io/docs/security/cves](https://docs.goauthentik.io/docs/security/cves). The project conducts external audits — see `website/docs/security/audits-and-certs/`.

### Hardening checklist

- [ ] `AUTHENTIK_SECRET_KEY` set to 60+ random chars, distinct per environment.
- [ ] PostgreSQL on a private network; `AUTHENTIK_POSTGRESQL__SSLMODE=require` or higher.
- [ ] Reverse proxy enforces TLS; never expose 9000/9443 directly.
- [ ] `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS` correctly scoped (don't trust the world).
- [ ] Default `akadmin` user disabled after creating a real admin.
- [ ] Recovery key generated and stored offline.
- [ ] MFA required for all admin users via flow policy.
- [ ] Reputation policy enabled to throttle brute force.
- [ ] GeoIP policy enabled if your user population is geographically constrained.
- [ ] Notification rules for `login_failed` (≥ N in M minutes) routed to a SIEM or chat.
- [ ] `AUTHENTIK_ERROR_REPORTING__ENABLED=false` if you don't want telemetry to Sentry.
- [ ] `AUTHENTIK_DISABLE_UPDATE_CHECK=true` + `AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true` for air-gapped.
- [ ] Outpost tokens rotated periodically (regenerate in UI, redeploy outpost with new token).
- [ ] Docker socket not mounted to worker, OR a socket-proxy is in place.
- [ ] `compliance.fips.enabled=true` if FedRAMP/FIPS 140-2 required.

> [!CAUTION]
> The internal debug endpoint at port **9900** exposes `pprof` profiles and `dump_config` — these reveal sensitive runtime state. The container does not expose this port publicly by default. Do not change that. Set `AUTHENTIK_DEBUG=false` (default) for production.

---

## 17. Performance Tuning

### Server (Django + uvicorn)

| Setting | Default | Tune to |
|---|---|---|
| `web.workers` (gunicorn processes) | 2 | `2 × CPU cores` |
| `web.threads` (threads per worker) | 4 | 4-8 for CPU-bound, 16+ for I/O-bound |
| `web.timeout_http_read` | 30 s | 60 s if flows have slow stages (e.g. captcha provider) |
| `web.timeout_http_idle` | 120 s | 300 s for long-poll-style integrations |
| `postgresql.conn_max_age` | 0 | 60 (persistent connections) |
| `postgresql.use_pool` | false | true (with `psycopg[pool]`) |
| `cache.timeout_flows` | 300 s | 600 s (longer cache → less PostgreSQL load; trade off vs. policy freshness) |

### Worker

| Setting | Default | Tune to |
|---|---|---|
| `worker.processes` | 1 | 2-4 for heavy sync workloads (LDAP, SCIM) |
| `worker.threads` | 2 | 4-8 |
| `worker.task_default_time_limit` | 10 m | 30 m if your largest sync exceeds 10 m |

### PostgreSQL

```sql
-- Reasonable starting point for a medium install
max_connections = 200
shared_buffers = 2GB             -- 25% of RAM
effective_cache_size = 6GB       -- 75% of RAM
work_mem = 16MB
maintenance_work_mem = 256MB
random_page_cost = 1.1           -- if on SSD
wal_buffers = 16MB
synchronous_commit = on          -- never set off for an IdP
```

Index hot-spots: `authentik_events_event.created`, `authentik_core_authenticatedsession.expires`, `authentik_providers_oauth2_accesstoken.expires`.

### Outpost scaling

- **LDAP outpost** — the `bind/memory/` backend dramatically reduces server load. Tune `ldap.page_size` (default 50) higher for large directories (500+ per page acceptable). Set `ldap.task_timeout_hours` to match your largest sync.
- **Proxy outpost** — use `postgresstore` session backend for sticky-session-free load balancing across multiple proxy outposts.
- **RADIUS outpost** — stateless and CPU-cheap; one outpost can handle thousands of req/s.

### Frontend

- The admin SPA's bundle is ~1 MB gzipped. The User and Flow bundles are smaller (~300 KB and ~150 KB).
- All web assets are served with `Cache-Control: max-age=31536000, immutable` (filenames are hashed).
- SFE bundle (for IE-11) is ~80 KB gzipped.

### Caching impact

The FlowPlanner cache (`cache.timeout_flows`) is the single highest-leverage cache. A cache hit returns a pre-computed FlowPlan without re-evaluating policies — for high-traffic flows this can reduce per-request DB queries by 90%+.

The policy decision cache (`cache.timeout_policies`) caches *individual policy* results keyed on (policy_id, user_id, request_hash). Useful for expensive Expression policies; less useful for fast policies (the cache lookup may cost as much as the evaluation).

---

## 18. Code Quality and Patterns

### Strengths

- **Consistent layering** — every Django app has the same structure (`models.py`, `api/`, `tests/`, `migrations/`, often `signals.py` and `tasks.py`). Onboarding a new contributor is fast.
- **Clear abstractions** — `Stage`, `Provider`, `Source`, `Policy`, `PropertyMapping` are well-defined base classes. Adding a new one is a recipe.
- **Type hints** — Python 3.14 with extensive type annotation; pydantic for validation.
- **Test coverage** — every stage, provider, source has its own `tests/` directory. The briefing's `confidence_gaps` shows 47 modules without tests, mostly in `web/` and template directories — Python coverage is high.
- **Separation of API and UI** — the SPA consumes the same REST API as third-party clients. The schema is the contract.
- **Outposts as first-class citizens** — running protocol handlers in their own process is the right call.
- **Blueprints** — declarative config from day one means GitOps is supported without bolt-on tooling.

### Rough edges

| Issue | Where | Impact |
|---|---|---|
| `cmd/server/server.go` `attemptProxyStart` polls every 1s up to 100 times | `cmd/server/server.go:73-100` | If the embedded outpost can't start, the server runs without it for ~100s before giving up. Could be event-driven. |
| `authentik/lib/default.yml` is the canonical config but every key needs a parallel entry in env-var parsing | `authentik/lib/config.py` | New config additions easy to forget on the env-var side. |
| Three runtimes (Python + Go + Rust) means three sets of CI, three sets of dependencies | repo-wide | Higher maintenance burden than a single-language codebase. |
| Migrations are forward-only and skip-version is unsupported | per [#22688](https://github.com/goauthentik/authentik/issues/22688) | Users must upgrade through each minor. |
| Web bundle is ~1 MB for the admin SPA | `web/src/admin/` | Heavy on first load. Code-splitting helps but not eliminated. |
| Rust frontend (`src/`) is in an in-between state | repo-wide | Two HTTP frontends in tree (Go `cmd/server` + Rust `src/main.rs`). Not yet clear which to use when. |
| Some EE features have non-obvious dependencies | `authentik/enterprise/` | E.g. RAC requires a separate outpost binary, but the UI doesn't tell you until after creation. |
| `cspell.config.jsonc` shows the project maintains its own dictionary of accepted misspellings | repo root | A long list — implies some legacy decisions that didn't get cleaned. |

### Patterns worth borrowing

- **`!Env`, `!Find`, `!KeyOf` YAML tags** — a clean blueprint language for declarative config. Many tools could learn from this.
- **Outpost-as-binary architecture** — using Go for the protocol surface and Python for the business logic is the right split for an IdP.
- **PostgreSQL-only stack** — the django-channels-postgres + django-postgres-cache + django-dramatiq-postgres combo eliminates Redis entirely. Powerful simplification.
- **Flow + Stage + Policy** — extensible-by-composition for authentication workflows. Adopted by Keycloak (in the Authentication SPI) and others.

---

## 19. Testing Strategy

### Python (`pytest`)

```bash
docker compose exec worker pytest authentik/
```

Tests cover models, ViewSets, flow executors, blueprint round-tripping, policy evaluation. Use `pytest-django` for DB fixtures, `pytest-mock` for mocking external calls.

Key test directories:
- `authentik/core/tests/`
- `authentik/flows/tests/`
- `authentik/blueprints/tests/`
- `authentik/providers/<n>/tests/`
- `authentik/sources/<n>/tests/`
- `authentik/stages/<n>/tests/`
- `authentik/policies/<n>/tests/`

### Go (`go test`)

```bash
go test ./...
```

Outposts have a smaller test footprint than the Python core, but each subpackage (`internal/outpost/ldap/`, `internal/outpost/proxyv2/`, etc.) has unit tests for handlers.

### Rust (`cargo test`)

```bash
cargo test --workspace
```

Tests the axum extensions, PROXY-protocol parser, and config loader.

### Integration (`tests/integration/`)

Python-driven tests that spin up the full stack via docker-compose, then exercise the API.

### End-to-end (`tests/e2e/` + `web/e2e/`)

Two layers:

1. `tests/e2e/` — docker-compose fixtures that test integrations with real LDAP servers, OAuth IdPs, Envoy/Nginx/Traefik forward-auth, etc.
2. `web/e2e/` — Playwright tests that drive a real browser through actual flows.

### OIDC conformance

`tests/openid_conformance/` runs the OpenID Connect Foundation conformance suite against a local authentik install. Run via `make test-openid-conformance`.

### Continuous integration

GitHub Actions workflows live in `.github/workflows/` (not in the briefing — see the repo). The README's badges show three pipelines: `ci-main` (core build/test), `ci-outpost` (Go binaries), `ci-web` (TS/web).

### Test gaps (from `metadata.json:1332-1346`)

The briefing's `confidence_gaps` reported `test_coverage` for 47 directories. Spot-checking:

- Several `migrations/` directories — typically intentional (migrations aren't unit-tested).
- A few smaller stages (`account_lockdown`, `mtls`) — possibly intentional for EE-only code under refactor.
- Some `web/src/elements/` subdirs — UI components without unit tests but covered by e2e.

This is a partial measurement, not a coverage report. For a true coverage map, use Codecov (the README badge links to the live report).

---

## 20. Suggested Improvements

In rough order of impact:

1. **Reduce admin SPA bundle size**. Code-split per route more aggressively; consider lazy-loading Monaco only when the blueprint editor opens (it accounts for a large chunk of the bundle).
2. **Migrate from Go embedded outpost to Rust frontend** (the partial work in `src/`). Once the Rust frontend ships, the embedded-outpost startup polling loop becomes unnecessary.
3. **Make migrations safe across minor versions**. Either add forward-only migration squashing every N releases, or document a "minor-by-minor upgrade" tool that automates the staged upgrade.
4. **Fix the OAuth single logout regression** ([#22572](https://github.com/goauthentik/authentik/issues/22572)). 14-comment thread, still open.
5. **Fix Safari admin crash** ([#19166](https://github.com/goauthentik/authentik/issues/19166)). 8-comment thread, references the older [#14322](https://github.com/goauthentik/authentik/issues/14322).
6. **Resolve the "2fa stage no identification data" regression** ([#19552](https://github.com/goauthentik/authentik/issues/19552)). 43 comments — high user impact.
7. **Fix invitation creation when User has custom required attributes** ([#22637](https://github.com/goauthentik/authentik/issues/22637)).
8. **Restore the email password-reset template grammar** ([#22757](https://github.com/goauthentik/authentik/issues/22757)).
9. **Add a CLI for outpost local development** — currently outpost contributors need a full authentik server running.
10. **Document the Rust frontend's role** — `src/main.rs` exists but its current production-readiness vs. `cmd/server/main.go` isn't documented anywhere. Decide and document.
11. **Standardize the outpost healthcheck mode arguments** — currently `healthcheck allinone` exists for the server binary but the other outpost binaries don't accept modes. Inconsistent.
12. **Reduce config drift between `default.yml` and `internal/config/config.go`** — adding a key requires updating both. Consider generating one from the other.
13. **Add per-flow rate limiting** — currently throttling is global or per-provider; per-flow would be useful for slowing brute force on a specific authentication flow.
14. **Expose more metrics** — the Prometheus surface is good but adding metrics for "active flows", "policy evaluation duration", "blueprint apply duration" would help operators.
15. **Investigate the `cspell.config.jsonc` accepted-misspellings list** — long enough to suggest some technical debt in naming.

---

## 21. Contributing

The project ships `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` at the repo root. Developer documentation at [docs.goauthentik.io/docs/developer-docs/](https://docs.goauthentik.io/docs/developer-docs/) covers:

- Setting up a dev environment (`uv` for Python, `node@24`, `go@1.22+`, Rust stable, PostgreSQL).
- Running the test suites.
- Code style: ruff + black for Python, gofmt for Go, rustfmt for Rust, prettier+eslint for TS.
- Documentation style: `website/docs/developer-docs/docs/style-guide.mdx` (referenced from README).
- Translation: managed via [Transifex](https://explore.transifex.com/authentik/authentik/).

### Make targets (from `Makefile`)

```text
make gen-dev-config           # Generate local dev config
make gen-client-ts            # Regenerate TypeScript SDK
make gen-client-go            # Regenerate Go SDK
make docs                     # Build docs site
make docs-watch               # Build + watch docs
make test                     # Run Python tests
make test-coverage            # Python + coverage report
make lint                     # Run all linters
make web                      # Build web bundle
make web-watch                # Build + watch web
```

### PR workflow

1. Fork on GitHub.
2. Branch from `main`.
3. Run `make lint test` locally.
4. Push, open PR.
5. CI runs (`ci-main`, `ci-outpost`, `ci-web`) — must be green.
6. Maintainer review; typically one round of feedback.
7. Squash-merge.

### Issue tracker

[github.com/goauthentik/authentik/issues](https://github.com/goauthentik/authentik/issues). Bug reports should use the template; feature requests via the same tracker.

### Community

- [Discord](https://goauthentik.io/discord) — primary real-time channel.
- [Discussions on GitHub](https://github.com/goauthentik/authentik/discussions).
- [@goauthentik on Mastodon](https://fosstodon.org/@goauthentik) for announcements.

---

## 22. Quick Reference

### Service-port matrix

| Container | Port | Default proto | Purpose |
|---|---|---|---|
| server | 9000 | HTTP | API + Web + embedded outpost |
| server | 9443 | HTTPS | API + Web (TLS) |
| server | 9300 | HTTP | Prometheus metrics |
| server | 9900 | HTTP | Debug (pprof, dump_config) — bind 0.0.0.0 default, scope away |
| ldap outpost | 3389 | LDAP | LDAP (StartTLS) |
| ldap outpost | 6636 | LDAPS | LDAP over TLS |
| radius outpost | 1812 | RADIUS/UDP | RADIUS |
| proxy outpost | 9000 | HTTP | Forward-auth + reverse-proxy |
| proxy outpost | 9443 | HTTPS | Forward-auth + reverse-proxy (TLS) |
| postgresql | 5432 | PostgreSQL | DB |

### Container image matrix

```text
ghcr.io/goauthentik/server:<version>       # main server + worker
ghcr.io/goauthentik/proxy:<version>        # proxy outpost
ghcr.io/goauthentik/ldap:<version>         # ldap outpost
ghcr.io/goauthentik/radius:<version>       # radius outpost
ghcr.io/goauthentik/rac:<version>          # rac outpost
```

Pattern is `ghcr.io/goauthentik/%(type)s:%(version)s` (configurable via `AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE`).

### Mount-point cheat sheet

| Path | Container | Purpose |
|---|---|---|
| `/etc/authentik/config.yml` | server, worker | Merged effective config (read-only) |
| `/blueprints/` | server, worker | Custom blueprints |
| `/data/` | server, worker | Uploaded files |
| `/certs/` | server, worker | Auto-discovered cert pairs |
| `/templates/` | worker | Custom email templates |
| `/geoip/` | server, worker | GeoLite2 databases |
| `/var/run/docker.sock` | worker | Optional, for outpost auto-deploy |

### Stage-type matrix

(Confirmed in `authentik/stages/` tree)

| Type | Module | License |
|---|---|---|
| identification | `authentik/stages/identification/` | core |
| password | `authentik/stages/password/` | core |
| user_login | `authentik/stages/user_login/` | core |
| user_logout | `authentik/stages/user_logout/` | core |
| user_write | `authentik/stages/user_write/` | core |
| user_delete | `authentik/stages/user_delete/` | core |
| prompt | `authentik/stages/prompt/` | core |
| consent | `authentik/stages/consent/` | core |
| email | `authentik/stages/email/` | core |
| captcha | `authentik/stages/captcha/` | core |
| invitation | `authentik/stages/invitation/` | core |
| deny | `authentik/stages/deny/` | core |
| redirect | `authentik/stages/redirect/` | core |
| source | `authentik/stages/source/` (EE-extended) | core/EE |
| dummy | `authentik/stages/dummy/` | core (test-only) |
| authenticator_static | `authentik/stages/authenticator_static/` | core |
| authenticator_totp | `authentik/stages/authenticator_totp/` | core |
| authenticator_webauthn | `authentik/stages/authenticator_webauthn/` | core |
| authenticator_duo | `authentik/stages/authenticator_duo/` | core |
| authenticator_sms | `authentik/stages/authenticator_sms/` | core |
| authenticator_email | `authentik/stages/authenticator_email/` | core |
| authenticator_validate | `authentik/stages/authenticator_validate/` | core |
| authenticator_endpoint_gdtc | `authentik/enterprise/stages/authenticator_endpoint_gdtc/` | **EE** |
| account_lockdown | `authentik/enterprise/stages/account_lockdown/` | **EE** |
| mtls | `authentik/enterprise/stages/mtls/` | **EE** |

### Provider-type matrix

| Type | Module | License | Outpost? |
|---|---|---|---|
| OAuth2 / OIDC | `authentik/providers/oauth2/` | core | no |
| SAML | `authentik/providers/saml/` | core | no |
| LDAP | `authentik/providers/ldap/` | core | yes (`ldap`) |
| RADIUS | `authentik/providers/radius/` | core | yes (`radius`) |
| Proxy | `authentik/providers/proxy/` | core | yes (`proxy` or embedded) |
| SCIM | `authentik/providers/scim/` | core | no |
| RAC | `authentik/providers/rac/` | **EE** | yes (`rac`) |
| MS Entra | `authentik/enterprise/providers/microsoft_entra/` | **EE** | no |
| Google Workspace | `authentik/enterprise/providers/google_workspace/` | **EE** | no |
| WS-Federation | `authentik/enterprise/providers/ws_federation/` | **EE** | no |
| SSF | `authentik/enterprise/providers/ssf/` | **EE** | no |

### Source-type matrix

| Type | Module | License |
|---|---|---|
| LDAP | `authentik/sources/ldap/` | core |
| OAuth | `authentik/sources/oauth/` | core |
| SAML | `authentik/sources/saml/` | core |
| Kerberos | `authentik/sources/kerberos/` | core |
| SCIM | `authentik/sources/scim/` | core |
| Plex | `authentik/sources/plex/` | core |
| Telegram | `authentik/sources/telegram/` | core |

### Policy-type matrix

| Type | Module | License |
|---|---|---|
| Expression | `authentik/policies/expression/` | core |
| Event Matcher | `authentik/policies/event_matcher/` | core |
| Password | `authentik/policies/password/` | core |
| Expiry | `authentik/policies/expiry/` | core |
| GeoIP | `authentik/policies/geoip/` | core |
| Reputation | `authentik/policies/reputation/` | core |
| Dummy | `authentik/policies/dummy/` | core (test-only) |
| Unique Password | `authentik/enterprise/policies/unique_password/` | **EE** |

### Useful one-liners

```bash
# Show the merged config in use
docker compose exec server cat /etc/authentik/config.yml

# Apply all blueprints in /blueprints/
docker compose exec worker ak apply_blueprint default/

# Export current state as a blueprint
docker compose exec worker ak apply_blueprint --export > snapshot.yaml

# Hash a password offline (for blueprint seeding)
docker compose exec worker ak hash_password 'correct horse battery staple'

# Smoke-test an LDAP source
docker compose exec worker ak ldap_check_connection my-ldap

# Trigger an on-demand LDAP sync
docker compose exec worker ak ldap_sync my-ldap

# Trigger an on-demand SCIM provisioning
docker compose exec worker ak scim_sync my-slack-provider

# Build a fresh OpenAPI schema
docker compose exec worker ak build_schema --api-file /tmp/schema.yml

# Generate a recovery key
docker compose exec worker ak create_recovery_key 60 akadmin

# Set a tenant feature flag
docker compose exec worker ak set_flag <flag_key> <flag_value>

# Convert a user to service account
docker compose exec worker ak change_user_type --type service_account alice
```

### Where to learn more

- **Official docs**: [docs.goauthentik.io](https://docs.goauthentik.io)
- **Architecture page**: [docs.goauthentik.io/docs/core/architecture](https://docs.goauthentik.io/docs/core/architecture)
- **Developer docs**: [docs.goauthentik.io/docs/developer-docs](https://docs.goauthentik.io/docs/developer-docs)
- **Style guide**: [website/docs/developer-docs/docs/style-guide.mdx](https://github.com/goauthentik/authentik/blob/main/website/docs/developer-docs/docs/style-guide.mdx)
- **Helm chart**: [github.com/goauthentik/helm](https://github.com/goauthentik/helm)
- **SECURITY.md**: [github.com/goauthentik/authentik/blob/main/SECURITY.md](https://github.com/goauthentik/authentik/blob/main/SECURITY.md)
- **Releases**: [docs.goauthentik.io/docs/releases](https://docs.goauthentik.io/docs/releases)
- **Discord**: [goauthentik.io/discord](https://goauthentik.io/discord)
- **Sponsor / Enterprise**: [goauthentik.io/pricing](https://goauthentik.io/pricing)

---

_Generated May 2026 from goauthentik/authentik at 9e75f62. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
