# Configuration

Everything is environment-driven, so the same images run on a laptop or in production. The essentials:

## Database

| Variable | Purpose |
|----------|---------|
| `DB_HOST` / `DB_PORT` / `DB_NAME` | PostgreSQL location |
| `DB_USERNAME` / `DB_PASSWORD` | credentials |
| `DB_SSLMODE` | TLS to PostgreSQL, the JDBC driver's `sslmode`: `disable`, `allow`, `prefer` (default: TLS when offered, certificate **not** verified), `require`, `verify-ca`, `verify-full`. Production: `verify-full` |
| `DB_SSL_ROOT_CERT` | path to the PEM CA bundle that signed the PostgreSQL server certificate; needed for `verify-ca` / `verify-full`. A `SPRING_DATASOURCE_URL` with its own `?sslmode=` still wins |
| `DB_ENCRYPTION` | **required** — 32-char hex key encrypting secrets at rest (TOTP seeds, signing keys, IdP secrets). **Immutable after first boot.** A stored secret that cannot be decrypted with it fails the read |
| `HELIX_ALLOW_PLAINTEXT_SECRETS` | `false`. Accepts running **without** `DB_ENCRYPTION` outside the `dev` profile (secrets then stored in plaintext). An empty `DB_ENCRYPTION` means no key |
| `HELIX_DB_ENCRYPTION_MIGRATE_PLAINTEXT` / `HELIX_DB_ENCRYPTION_MIGRATE_UNTIL` | `false` / empty. Migration mode after adding `DB_ENCRYPTION` to an install that ran without it: plaintext secret rows are read as stored and encrypted on their next write. Loud (ERROR banner at startup and hourly) and time-boxed: outside `dev`, the server refuses to start unless `…_UNTIL` is a date (`yyyy-MM-dd`) at most 30 days ahead, and after that day (UTC) plaintext reads fail closed. See the [upgrade guide](../operations/upgrade-rc8-rc9.md#5-plaintext-secret-rows-a-time-boxed-migration-window) |
| `HELIX_DB_ENCRYPTION_ALLOW_LEGACY` | `false`. Values in the legacy AES/ECB format are re-encrypted to AES-GCM at every startup; afterwards the legacy format is no longer read unless this is `true` |

## Session store (Redis)

HelixIAM keeps HTTP sessions in Redis by default.

| Variable | Purpose |
|----------|---------|
| `REDIS_HOST` / `REDIS_PORT` | Redis location for the HTTP session store |

Redis is optional: set `HELIX_SESSION_STORE=queue` (and leave `HELIX_TOKEN_STORE` on its `queue` default) to keep everything in PostgreSQL.

## Notifications (SMTP, SMS, push)

Message delivery isn't an environment variable — it's **per-realm configuration** you set from the
console or admin API (SMTP/HTTP for email, Twilio/HTTP for SMS, FCM/APNs for push), so each realm/tenant
can send from its own accounts. See [Notifications](../operations/notifications.md).

## External URLs

| Variable | Purpose |
|----------|---------|
| `IDP_BASE_URL` | the IdP's own external base URL (issuer / federation callbacks) |
| `SP_BASE_URL` | the primary app/console origin (CORS) |
| `HELIX_SAML_IDP_ENTITY_ID` | SAML IdP entity id |

## Reverse proxies and host names

| Variable | Default | Purpose |
|----------|---------|---------|
| `HELIX_TRUSTED_PROXIES` | *(empty)* | comma-separated IPs or CIDR ranges (IPv4/IPv6) of the proxies whose forwarded headers are honoured: `X-Forwarded-For` for the client IP, `Forwarded` / `X-Forwarded-Proto` / `-Host` / `-Port` / `-Prefix` for the scheme, host and issuer. **Required behind a TLS-terminating proxy.** `X-Real-IP` is never read. Helm: `config.trustedProxies` |
| `HELIX_ALLOWED_HOSTS` | *(empty)* | host names answered besides the hosts of `IDP_BASE_URL` and `SP_BASE_URL` (outside `dev`, any other `Host` gets `400`); `name` accepts any port, `name:port` or `[v6]:port` only that port. Helm: `config.allowedHosts` |
| `HELIX_ADMIN_ALLOWED_HOSTS` | *(empty)* | when set, the admin REST API (`/admin/**`, `/v3/api-docs/**`, `/swagger-ui/**`) and the master realm answer only on these hosts (the admin console's host), `404` elsewhere. Helm: `config.adminAllowedHosts`. See [Install](install.md#admin-api-only-on-the-admin-host) |
| `HELIX_ADMIN_RESTRICT_MASTER_REALM` | `true` | with `HELIX_ADMIN_ALLOWED_HOSTS`: also restrict `/realms/master/**` to the admin hosts. Helm: `config.adminRestrictMasterRealm` |
| `HELIX_CONSOLE_BASE_URL` | *(unset)* | the admin console's origin, used for the built-in `helix-console` client's redirect URI; the Helm chart sets it from `config.spBaseUrl` when the console is enabled |
| `HELIX_COOKIE_SECURE` | `true` | secure cookies; the CSRF cookie is then `__Host-XSRF-TOKEN`. `false` (plain HTTP) is for development and demos only. Helm: `config.cookieSecure` |

See [Security hardening — Reverse proxies and host names](../operations/security.md#reverse-proxies-and-host-names).

!!! info "Use https in production"
    WebAuthn passkeys require a secure origin. Set the base URLs to real `https://` hostnames.

## Outbound calls (egress)

Server-initiated calls to admin- or client-configured URLs (webhooks, SCIM, HTTP email/SMS/push, SMTP relays, LDAP, OIDC/eID brokers, JWKS, logout, wallet status lists and CRL/OCSP) pass an egress guard: loopback, private, link-local and cloud-metadata destinations are refused, the host is resolved once and the connection goes to the checked address. These settings are deployment-wide; a realm admin cannot widen them.

| Variable / property | Default | Purpose |
|----------|---------|---------|
| `HELIX_EGRESS_ALLOWED_PRIVATE_HOSTS` | *(empty)* | private or loopback hosts outbound calls may reach (in-cluster mailer, HTTP gateway): exact host names or IP literals, optionally `host:port`. Helm: `config.egressAllowedPrivateHosts` |
| `HELIX_MESSAGING_SMTP_ALLOWED_PORTS` | *(empty)* | SMTP ports allowed besides 25, 465, 587 and 2525, e.g. `1025,8025-8030`; applies to realm relays and the global relay |
| `HELIX_FEDERATION_LDAP_ALLOWED_HOSTS` | *(empty)* | internal LDAP directories: host names (`ldap.corp`, `ldap.corp:636`) or CIDR ranges (`10.20.0.0/16`); empty = public directories only. Helm: `config.ldapAllowedHosts` |
| `HELIX_EGRESS_ALLOW_PRIVATE` | `false` | **deprecated**: opens every outbound URL to private and loopback addresses; use the allowlist |
| `helix.outbound.fetch.<group>.max-concurrent` / `.per-realm-max` | 32 / 8 (`wallet`: 16 / 16) | concurrent outbound calls per feature group — `login-admin`, `login-client`, `otp`, `outbound`, `ldap`, `wallet` — and per realm |
| `helix.outbound.deliveries.max-pending` | `20000` | pending webhook / SCIM deliveries per queue across all realms (1,000 per realm) |

See [Upgrading to rc.8 / rc.9](../operations/upgrade-rc8-rc9.md#15-smtp-relays-pass-the-egress-guard).

## Bootstrap admin

| Variable | Purpose |
|----------|---------|
| `HELIX_ADMIN_USERNAME` / `HELIX_ADMIN_PASSWORD` | the master-realm admin created on first boot; the username defaults to `admin` |
| `HELIX_ADMIN_PASSWORD_FILE` | where a generated password is written (mode `0600`) when `HELIX_ADMIN_PASSWORD` is unset; default `$TMPDIR/helixiam-admin-password` (`/tmp/helixiam-admin-password` in the image). The password is never logged, only this path |

## Feature toggles

| Variable | Default | Purpose |
|----------|---------|---------|
| `USER_REGISTRATION_ENABLED` | `false` | platform-wide master switch for self-registration; each realm also has its own **Allow self-registration** toggle, and both must be on for that realm's `/register` to accept sign-ups — see [Realm settings — Registration](../operations/realm-settings.md#registration) |
| `MFA_ENABLED` | `true` (`false` in the `dev` profile) | `true` sends every password sign-in in every realm through TOTP; `false` leaves it to each realm's `requireMfa` |
| `HELIX_SESSION_STORE` | `redis` (`queue` in the `dev` profile) | HTTP session store: `redis`, or `queue` to keep sessions in PostgreSQL |
| `HELIX_TOKEN_STORE` | `queue` | `queue` = the built-in Postgres-backed token store; `redis` for the high-throughput Redis tier |
| `HELIX_FEDERATION_STORE_ENABLED` | `true` | load identity providers created in the console / admin API into the live registry (every realm) |
| `HELIX_FEDERATION_STORE_POLL_INTERVAL_SECONDS` | `10` | how often each replica checks for identity-provider changes made on another replica; `0` = off |
| `HELIX_WALLET_TRANSACTION_STORE` | *(automatic)* | EU Digital Identity Wallet transaction store: `jdbc`, `redis` or `memory` (one replica, development only). Automatic: PostgreSQL, or Redis when the session store is Redis — see [the wallet guide](../federation/eudi-wallet.md#transaction-store-and-multiple-replicas) |
| `HELIX_CLUSTER_STATE_STORE` | *(automatic)* | where flow state (push, QR, transaction signing, enrolment, CIBA, SAML replay cache) and rate limits are shared: `jdbc`, `redis` or `memory` (one replica only). Automatic: Redis with the Redis session store, PostgreSQL otherwise — see [High availability](../operations/high-availability.md) |
| `HELIX_CLUSTER_EXPECTED_REPLICAS` | `0` | expected number of replicas (the Helm chart sets it from `replicaCount`); above 1, keeping shared state in memory logs a WARN |
| `HELIX_RETENTION_ENABLED` | `true` | the scheduled cleanup of expired rows — see [Data retention](../operations/data-retention.md) |
| `HELIX_RETENTION_AUDIT_LOG_MAX_AGE` | *(empty: keep)* | delete audit-log rows older than this (e.g. `400d`); empty keeps the audit log |
| `HELIX_FLOW_ENGINE_ENABLED` | `false` | drive post-password steps with the flow engine |
| `HELIX_MIGRATIONS_ENABLED` | `true` | Flyway manages the schema (see [Upgrades](../operations/upgrades.md)) |
| `HELIX_AUDIT_ENABLED` | `true` | emit the audit log |

`DB_RO_HOST` (and `spring.readonly.datasource.url`) are deprecated and ignored from 1.0.0-rc.13: the server uses one
connection pool and logs a warning at startup when either is set.

!!! danger "Never change DB_ENCRYPTION after first boot"
    Existing encrypted rows become unreadable. Back the key up and keep it stable for the life of the
    install.

The full list, with defaults, is in the server's `application.properties` and the product README's *Configuration* section.
