Skip to content

ADR 0003: Authentik for Authentication

Status: Accepted Date: 2026-04-29

Context

SCALR needs unified authentication across multiple apps. A user logs in once at the portal and is recognized by every other app they're entitled to use. This rules out per-app auth and points squarely at a central identity provider issuing OIDC tokens.

The realistic candidates were:

  • Keycloak — battle-tested enterprise IdP from Red Hat. Massive feature set.
  • Authentik — modern open-source IdP with a visual flow builder. Cleaner UX than Keycloak.
  • Ory (Kratos + Hydra + Keto + Oathkeeper) — modular, API-first, four services to operate.
  • Authelia — forward-auth proxy gate, not a full IdP.
  • Build our own — JWT issuing, JWKS rotation, refresh, MFA, password reset.

Decision

Use Authentik, self-hosted in services/auth/.

Reasons specific to SCALR's situation:

  1. Stack fit. Authentik already wants Postgres + Redis, both of which we run anyway for the rest of the platform.
  2. Operational simplicity. Single Docker container for the server + one for the worker. Slots into the root docker-compose.yml cleanly.
  3. Standards-compliant. Issues real OIDC/JWT tokens, so our SDKs (auth-client-py, auth-client-ts) are written against the OIDC standard, not Authentik specifically. Provider lock-in is contained to services/auth/ configuration only.
  4. Visual flow builder. When we eventually want MFA, social login, or custom signup flows, we configure them in the UI instead of writing code.
  5. Right-sized. Heavier than Authelia, far lighter than Keycloak. Fits a single-operator home lab without enterprise overhead.

Why not the others

  • Keycloak. Production setup is genuinely difficult — SSL, proxies, hostname configuration, and documentation gaps. Massive feature set we don't need yet. Right answer if/when we hit enterprise federation needs, not before.
  • Ory. Modular by design — running 3-4 services for what Authentik does in one. Too many moving parts for current scale.
  • Authelia. Doesn't issue OIDC tokens. Wrong tool — apps can't get user identity from it cleanly.
  • Build our own. JWT signing, JWKS rotation, refresh tokens, password reset, MFA — too much to get right ourselves, with no upside.

Consequences

Positive: - Standard OIDC means the SDKs are simple wrappers around well-tested libraries (authlib in Python, oidc-client-ts in TypeScript). - Authentik's "Application" + "Group" model maps cleanly to our entitlement needs. - The portal queries Authentik for the user's entitled applications and intersects with discovered manifests — see 0005-app-manifest-schema.md.

Negative: - Authentik's data model (flows, stages, providers, applications) takes a few hours to understand the first time. - We're tied to running another stateful service. Mitigated by it sharing Postgres and Redis with everything else.

Migration path

If Authentik ever has to be replaced, only services/auth/ configuration changes. The SDKs and apps don't change because they speak generic OIDC.