High availability¶
Run two or more replicas of helix-iam-server behind a load balancer. Every request may land on any replica: state
that a sign-in needs from one request to the next lives in PostgreSQL (or Redis, if you run it), never in one
replica's memory. The Helm chart's default is replicaCount: 2.
Changed in 1.0.0-rc.13
Up to and including 1.0.0-rc.12, push approval, QR sign-in, transaction signing, device enrolment, CIBA, the SAML assertion replay cache and the rate limits were kept in each replica's memory: a flow that started on one pod could fail on another, and N replicas gave N rate-limit budgets. 1.0.0-rc.13 moved them into the shared store described below.
What is shared, and where¶
| State | Where it lives |
|---|---|
| Browser sessions | config.sessionStore (HELIX_SESSION_STORE): redis (default) or queue (PostgreSQL) |
| Authorization codes, tokens, SSO sessions | config.tokenStore (HELIX_TOKEN_STORE): queue (PostgreSQL, default) or redis (the high-throughput tier) |
| Push approval, QR sign-in, transaction signing, device enrolment, CIBA, SAML assertion replay cache | the shared flow-state store: PostgreSQL table flow_state, or Redis keys helix:flow:* |
| Security rate limits (sign-in, MFA, registration, password reset, token endpoint, wallet, magic link, verification e-mail, account console) | the shared flow-state store: PostgreSQL table rate_limit_bucket, or Redis keys helix:rl:*; counted once for the whole cluster |
| EU Digital Identity Wallet transactions | PostgreSQL or Redis, see the wallet guide |
| Account lockout counters | PostgreSQL, one locked row per account |
The flow state and rate limits follow the stores: Redis when config.sessionStore is redis (or
config.tokenStore is redis and the session store is not queue), PostgreSQL otherwise.
config.clusterStateStore (HELIX_CLUSTER_STATE_STORE: jdbc, redis or memory) overrides that choice;
memory is for a single replica only.
Every transition of a flow (approve, deny, sign, confirm, consume, poll) is an atomic compare-and-set, so two
replicas racing on the same flow cannot both win. Flow ids are stored as SHA-256 hashes and the payload (challenges,
nonces, rotating QR tokens) is encrypted with DB_ENCRYPTION. Flyway migration V87 creates the two tables; expired
rows are swept.
Running without Redis¶
Redis is used only by the two stores above. Put both on queue, leave redis.host empty, and everything (sessions,
tokens, flow state, rate limits, wallet transactions) lives in PostgreSQL:
helm install helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 ... --set config.sessionStore=queue # no redis.host
The chart then sets no REDIS_HOST, its NetworkPolicy opens no Redis port, and the server never contacts Redis (its
health indicator is off, so readiness does not depend on it). The release gate boots this configuration
on a fresh k3d cluster without any Redis. The
municipality profile runs this way, with config.clusterStateStore: jdbc.
If you do run Redis, configure it with maxmemory-policy noeviction: an evicting Redis could drop a drained
rate-limit bucket, or a session, under memory pressure. The server logs a WARN at startup when it can read another
policy.
Warnings at startup¶
- The chart passes
replicaCountto the server asHELIX_CLUSTER_EXPECTED_REPLICAS. A server that keeps shared state in memory while more than one replica is likely (that setting above 1, or a Kubernetes environment without it) logs a WARN. - If PostgreSQL or Redis is briefly unreachable, a replica counts rate limits on its own rather than failing requests, and logs it.
Still per replica, by design¶
These protect the pod's own resources and are not a security budget: the outbound fetch concurrency caps
(helix.outbound.fetch.*), the e-mail send caps, and the workload-identity token-exchange probe limiter (the token's
signature is the control).
Chart values for availability¶
| Value | Purpose |
|---|---|
replicaCount |
number of server replicas (default 2) |
strategy |
the Deployment's update strategy; RollingUpdate with maxUnavailable: 0 keeps capacity during a roll |
topologySpread.enabled |
spread replicas over zones and nodes |
podDisruptionBudget.minAvailable |
replicas kept during voluntary disruptions such as node drains |
The municipality profile sets all four (three replicas, at least two available). For the database itself, use a highly available PostgreSQL with point-in-time recovery; see Backup & disaster recovery.
Upgrading to a release with the shared store¶
Flows in progress during the upgrade (push, QR, transaction signing, enrolment, CIBA) are lost, as they were in
memory; users start them again. Rate-limit budgets start fresh. A custom PushApprovalStore, QrSessionStore,
TransactionSigningStore or DeviceEnrollmentTicketStore bean must implement the new replace method.