First step toward Redis-backed sessions. Adds the shared `ioredis`
connection that every downstream consumer (session storage, OBO
token cache, …) will inject via the new `REDIS_CLIENT` DI token.
What lands:
- `ioredis@^5.10.1` as a direct dependency. Chosen by ADR-0010 for
its mature Sentinel support — single-instance URL today,
Sentinel-HA configuration plumbing lands with the production
infrastructure ADR.
- `.env.example` promotes `REDIS_URL` from its previous future-vars
comment block into an active variable, with a default that
matches `infra/local/.env` (REDIS_PASSWORD + REDIS_PORT). The
Sentinel-style keys (`REDIS_SENTINEL_HOSTS`,
`REDIS_SENTINEL_NAME`, `REDIS_TLS`) stay in the future-vars
comment until the prod deploy lands.
- `apps/portal-bff/src/config/check-redis-config.ts` — boot-time
guard mirroring the existing four:
`assertDatabaseUrl` / `assertEntraConfig` /
`assertSessionSecret`. Refuses to start if `REDIS_URL` is unset,
not a valid `redis://` / `rediss://` URL, missing the password
(the local stack requires one), or still set to the
`redis_dev_change_me` .env.example placeholder. Returns a typed
`RedisConfig` with parsed `host` + `port` for downstream
observability.
- `apps/portal-bff/src/redis/redis.token.ts` — `REDIS_CLIENT`
string token + `Redis` type alias. Same shape as
`ENTRA_CONFIG` / `MSAL_CLIENT`.
- `apps/portal-bff/src/redis/redis.module.ts` — `RedisModule`
exposes a factory provider for `REDIS_CLIENT`. The factory
builds the `ioredis` client from the parsed config, caps
`maxRetriesPerRequest` at 3 (so an unreachable Redis surfaces a
command-time error instead of an infinite reconnect storm), and
wires `connect` / `ready` / `error` / `close` / `reconnecting`
events into the Pino stream with the `redis` Pino context.
Non-global on purpose — modules import it to state "I depend on
Redis".
- `main.ts` calls `assertRedisConfig()` alongside the other three
validators. `AppModule` imports `RedisModule`.
Verification:
- `nx run-many -t lint test build --projects=portal-bff` — green.
- 62 / 62 specs (was 52; +10 across the config validator spec and
the module spec — the latter exercises both the happy path
against an unreachable URL — `ioredis` constructs lazily so no
real socket opens — and the missing-env failure mode).
- Boot smoke (with the local Compose stack running): the `redis`
Pino context shows `redis.connect` → `redis.ready` lines on
startup; killing the Redis container later produces
`redis.close` / `redis.reconnecting`.
What this PR explicitly does NOT do:
- Mount `express-session` + `connect-redis` middleware. The next
PR wires the session cookie (`__Host-portal_session`) + the
encrypted payload + the lookup middleware that attaches `user` to
every request.
- Plug the callback into session creation. Auth still ends with a
Pino log + redirect; the SPA still sees the user anonymous on the
next request.