# Architecture

HelixIAM ships as **two container images** that are deployed and scaled independently: the **server** and the **admin console**. They are separate products: scaling one never scales the other.

| Component | Image | What it does | Replicas |
| --- | --- | --- | --- |
| **Server** (`helix-iam-server`) | `ghcr.io/melmarzgioui/helixiam/helix-iam-server` | The OAuth2 / OIDC / SAML 2.0 endpoints, the sign-in, registration and MFA pages, the admin REST API, and the end-user **account console** (`/realms/{realm}/account`, rendered by the server). Owns one PostgreSQL database directly; no message broker | **n**, stateless across replicas: scale with sign-in traffic (2 for high availability, 3+ for several tenants) |
| **Admin console** (`helix-dashboard`) | `ghcr.io/melmarzgioui/helixiam/helix-dashboard` | The operator UI: a static React app served by nginx, which reverse-proxies `/admin`, sign-in and logout, the OIDC endpoints and the account API to the server, so the browser stays on one origin. Holds no state and no database connection | Its **own** count, typically **1–2**: operators, not end users. Can be internal-only |

Both images are cosign-signed, with an SPDX SBOM attested to each image (`cosign verify-attestation --type spdxjson`). Platform: from 1.1.0-rc.12 the images are linux/amd64 only; arm64 images are planned to return (earlier releases published both). Both are delivered to licensed customers from KubeDNA B.V.'s registry. The console image is first published with the release after `v1.1.0-rc.1`.

```mermaid
flowchart LR
  user([End users and relying parties]) -->|"login.example.nl (no /admin, no master realm)"| ing1[Public ingress]
  ops([Operators]) -->|beheer.example.nl| ing2[Internal ingress]
  ing1 --> srv
  ing2 --> con
  subgraph cluster[Kubernetes namespace]
    con["Admin console<br/>helix-dashboard (nginx + SPA)<br/>console.replicaCount: 1-2"]
    srv["Server<br/>helix-iam-server<br/>replicaCount: n (2, 3, ...)<br/>OIDC / SAML / admin API / account console"]
    con -->|"/admin, sign-in, OIDC, account API"| srv
  end
  srv --> pg[(PostgreSQL)]
  srv -.->|optional| redis[(Redis)]
```

The same in plain text:

```
  end users ──► public ingress (login.example.nl) ──────────────► server pods × n ──► PostgreSQL
                                                                     ▲   (Redis optional)
  operators ──► internal ingress (beheer.example.nl) ──► console pods × 1-2
                                                         (nginx: SPA + proxy of /admin, sign-in, account API)
```

## The server

**helix-iam-server** is the whole backend: it serves the OAuth2/OIDC endpoints (authorize, token, userinfo, JWKS, discovery, logout), the SAML 2.0 IdP, the sign-in pages and the realm-scoped admin REST API, and it owns the PostgreSQL database directly — no separate persistence tier, no cross-service calls.

- **Stateless across replicas.** Browser sessions, authorization codes and tokens, SSO sessions, flow state and rate limits live in PostgreSQL (or Redis for the high-throughput tier), never in one pod's memory, so any replica can answer any request and replicas can be added or removed at any time. The Helm chart passes `replicaCount` to the server as `HELIX_CLUSTER_EXPECTED_REPLICAS`, and the server warns when shared state is configured in memory with more than one replica. See [High availability](../operations/high-availability.md).
- **Scales on its own.** `replicaCount`, the PodDisruptionBudget and `topologySpread` in the chart apply to the server only.
- **PostgreSQL** holds all durable state — users, clients, roles, keys, sessions, audit. The connection can use TLS (`DB_SSLMODE`). **Redis** optionally backs the session and token stores ([Configuration](configuration.md)). **SMTP** (or another configured provider) delivers notifications.
- **CSRF.** Session-cookie writes echo the JavaScript-readable CSRF cookie in `X-XSRF-TOKEN`: `__Host-XSRF-TOKEN` in production (Secure, no `Domain`, so subdomains cannot plant one), `XSRF-TOKEN` on a plain-HTTP development server. It is always written at path `/`, so the console, `/account.html` and the realm URLs see the same token. Server-rendered forms carry `_csrf` instead. See [Authenticating to the API](api-authentication.md).

## The account consoles

There are two end-user account consoles:

- **The server-rendered account console** at `/realms/{realm}/account` (profile and **Edit details**, password, MFA devices, sessions, linked identities, wallet), rendered by the server from its own templates, on the server's host. **This is the account page users are sent to.** The server also serves the account JSON API under `/realms/{realm}/account/**` (`/profile`, `/editable-attributes`, `/credentials`, `/sessions`, …).
- **A single-page account console in the admin console image.** `helix-dashboard` also builds `account.html`; its nginx serves it at `/account.html` on the console's origin and proxies its calls to the account JSON API above. It only exists where the console is deployed.

Both edit the realm's self-editable profile attributes. See [Account console](../integration/account-console.md).

## The admin console

- **A separate image and a separate Deployment** (Helm: `console.enabled=true`; off by default). Its own `console.replicaCount`, optional HPA (`console.autoscaling`), PodDisruptionBudget, topology spread, Service, Ingress on its own host and NetworkPolicy. See [Install](install.md#the-admin-console-as-its-own-deployment).
- **Same origin by proxy.** The console's nginx forwards the admin API, sign-in and the OIDC endpoints to the server Service (`console.apiTarget`, by default the release's server Service), so the session cookie is never cross-site. Its origin is `config.spBaseUrl` (defaulted from the console Ingress host), which the server also uses as the origin of the built-in `helix-console` OIDC client (`HELIX_CONSOLE_BASE_URL`, redirect URI `<origin>/console/callback`).
- **Can be internal-only.** Its NetworkPolicy admits only the ingress controller you name and lets it reach only DNS and the server pods; put its Ingress on an internal IngressClass or an IP allow-list.
- **Administration only on the admin host.** With `HELIX_ADMIN_ALLOWED_HOSTS` (Helm `config.adminAllowedHosts`) set to the console host, the server answers the admin REST API (`/admin/**`, with `/v3/api-docs/**` and `/swagger-ui/**`) and the master realm (`/realms/master/**`; `HELIX_ADMIN_RESTRICT_MASTER_REALM`, default true) only on that host, and `404` on every other host. The host is the `Host` header, or `X-Forwarded-Host` from a trusted proxy only. Other realms are unaffected. Empty (the default) keeps the admin API on every allowed host.
- **Forwarded headers.** The console's nginx passes the ingress's `X-Forwarded-Proto/-Host/-Port` on unchecked, and the server trusts the console pods as proxies. The console's ingress must therefore overwrite any client-sent `X-Forwarded-*` headers (do not enable ingress-nginx `use-forwarded-headers` or Traefik `forwardedHeaders.insecure` for it), and the console Service must never be exposed directly (no LoadBalancer or NodePort): otherwise a client could choose the host the server sees, including an admin host.
- **Runs locked down.** nginx as uid 101, read-only root filesystem, all capabilities dropped, no service-account token. `/healthz` is answered by nginx itself, so a server outage does not take the console out of rotation.

## Request flow (human login)

1. An app redirects the browser to `…/realms/<realm>/oauth2/authorize` (OIDC) or POSTs a SAML `AuthnRequest`.
2. The server runs the realm's [authentication flow](../authentication/flows.md) (password, MFA, …),
   reading/writing session state in Postgres/Redis.
3. On success it issues an authorization code (OIDC) or SAML assertion; the app exchanges the code at the
   token endpoint for ID, access and refresh tokens signed by the realm's [keys](../operations/realm-keys.md).

## Request flow (machine, workload & agent token exchange)

Not every caller is a browser. The server also mints tokens for non-interactive callers with **no
session and no stored secret**:

1. A caller presents a credential — a client's `client_credentials`, a Kubernetes/CI **workload JWT**
   ([Workload identity](../integration/workload-identity.md)), or, for **on-behalf-of delegation**, a
   user `subject_token` + an [agent](../integration/agents.md) `actor_token`
   ([Agent authorization](../integration/agent-authorization.md)).
2. The server verifies it against the configured external issuer's JWKS (workload) or the realm's own
   signing key (delegation), and enforces the agent's lifecycle gate (a suspended agent is denied here).
3. It mints a short-lived realm-signed access token — carrying `nhi: true` for non-human callers, and an
   `act` claim naming the acting agent for delegated tokens. Endpoints include `…/oauth2/token`,
   `…/workload-identity/token`, and `…/agent/delegation/token`. Delegation with an `actor_token` runs only at
   `…/agent/delegation/token`; `…/oauth2/token` refuses `actor_token` with `invalid_request`.

See [Observability](../operations/observability.md) for the metrics the server exposes and
[Backup & DR](../operations/backup.md) for what to back up (just the database).
