# BFF environment template # Copy to .env (which is gitignored) and fill in actual values for local development. # Production values are managed by the platform's secret manager (see future infrastructure ADR). # Postgres connection (per ADR-0006) # Local dev default: dockerised Postgres on port 5432, schema 'public'. # Username / password / db must match infra/local/.env (POSTGRES_USER / # POSTGRES_PASSWORD / POSTGRES_DB) — those are the source of truth, # this is the BFF view of the same connection. # # IMPORTANT — URL encoding. The password is part of the URL userinfo # segment, so any of these characters must be URL-encoded: # @ → %40 # → %23 : → %3A / → %2F ? → %3F # % → %25 & → %26 = → %3D + → %2B ; → %3B # i.e. if your POSTGRES_PASSWORD is "p@ss#1", DATABASE_URL must read # "postgresql://portal:p%40ss%231@localhost:5432/portal_dev?schema=public" # The BFF aborts at boot with a clear error if it detects an unencoded # special character (see apps/portal-bff/src/config/check-database-url.ts). DATABASE_URL="postgresql://portal:portal_dev_change_me@localhost:5432/portal_dev?schema=public" # Observability (per ADR-0012) # All OTEL_* keys are honoured by the OpenTelemetry SDK directly — see # apps/portal-bff/src/observability/tracing.ts for the bootstrap. # Pino log level: 'info' in prod, 'debug' in dev (default if unset). LOG_LEVEL=debug OTEL_SERVICE_NAME=portal-bff OTEL_SERVICE_VERSION=dev # Default endpoint targets the Collector provisioned in # infra/local/dev.compose.yml. The /v1/traces suffix is required by # the HTTP/Protobuf transport. OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # v1 samples 100 % at the app; tail sampling is delegated to the # Collector (per ADR-0012). Override only for spike investigations. OTEL_TRACES_SAMPLER=always_on # Identity / Entra ID app registration (per ADR-0008 / ADR-0009) # Values come from the project's Entra application registration in the # Azure Admin Center → App registrations → APF Portal. The four # *_INSTANCE_URL / *_TENANT_ID / *_CLIENT_ID / *_CLIENT_SECRET keys # are mandatory; the BFF refuses to boot without them (see # apps/portal-bff/src/config/check-entra-config.ts). # # ENTRA_INSTANCE_URL is the Microsoft login endpoint — usually # https://login.microsoftonline.com/. The authority used by MSAL is # `${ENTRA_INSTANCE_URL}${ENTRA_TENANT_ID}` for single-tenant flows, # or `${ENTRA_INSTANCE_URL}organizations` / `common` for multi-tenant # (per ADR-0008's dual-audience design). v1 uses the tenant-scoped # authority; the multi-tenant switch lands when External ID activation # is needed. # # ENTRA_CLIENT_SECRET is the high-value secret of this set. Never # commit a real value. Production manages it via the deploy platform's # secret manager (future infrastructure ADR). ENTRA_INSTANCE_URL=https://login.microsoftonline.com/ ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000 ENTRA_CLIENT_ID=00000000-0000-0000-0000-000000000000 ENTRA_CLIENT_SECRET=replace_with_real_value # Redirect URIs registered in Entra alongside the same client id. Both # `/auth/callback` and `/auth/logout` paths are mounted by the BFF # once the OIDC routes land in a subsequent PR. ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/callback ENTRA_POST_LOGOUT_REDIRECT_URI=http://localhost:4200/ # Cookie signing secret (per ADR-0009 §"Cookies"). Used to sign the # transient pre-auth cookie that carries the OIDC `state` + PKCE # verifier between the /auth/login redirect and the /auth/callback # round-trip, and (once ADR-0010 ships) the session cookie's # integrity layer. Mandatory at boot — the BFF aborts if missing or # obviously weak (less than 32 base64-decoded bytes ≈ 256 bits of # entropy). Generate a fresh value per environment: # # node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" SESSION_SECRET=replace_with_32_random_bytes_base64url # Redis connection (per ADR-0010). The BFF uses `ioredis` for session # storage (today: just the connection; the express-session + # connect-redis middleware lands in the next PR). # # REDIS_URL — full URL form including auth. Must match `infra/local/.env` # (REDIS_PASSWORD + REDIS_PORT) when running against the local Compose # stack. Production wiring uses Sentinel (REDIS_SENTINEL_HOSTS + # REDIS_SENTINEL_NAME — future-vars block below) and TLS; the current # variable supports the dev single-instance shape only. REDIS_URL=redis://default:redis_dev_change_me@localhost:6379/0 # Session payload encryption (per ADR-0010 §"At-rest encryption"). # AES-256-GCM key for encrypting the session JSON that connect-redis # writes to Redis, so a Redis dump never carries raw user identities # / future tokens / claims in plaintext. **Distinct** from # SESSION_SECRET, which only signs the cookie's session-id — never # reuse one for the other. Mandatory at boot. # # node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" SESSION_ENCRYPTION_KEY=replace_with_32_random_bytes_base64url # Session timeouts (per ADR-0010). Both optional with sensible # defaults; override only when staging / prod policy diverges. # SESSION_IDLE_TIMEOUT_SECONDS — sliding window. Each request # extends the cookie's `expires` by this many seconds. # Default 1800 (30 min). # SESSION_ABSOLUTE_TIMEOUT_SECONDS — hard ceiling. Session is # destroyed regardless of activity at this age. # Default 43200 (12 h). # SESSION_IDLE_TIMEOUT_SECONDS=1800 # SESSION_ABSOLUTE_TIMEOUT_SECONDS=43200 # Per-environment salt used to pseudonymise the user id before it # lands in audit rows (per ADR-0013 §"Schema") and in Pino app log # lines (per ADR-0012 §"User id hashing"). Same value must be used # on both sides so audit and app logs join on `actor_id_hash`. # # Rotation invalidates the join key — old rows / log lines can no # longer be correlated with the new hash. Treat as long-lived per # environment. Mandatory at boot. # # node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" LOG_USER_ID_SALT=replace_with_32_random_bytes_base64url # Future env vars introduced by upcoming phases / ADRs: # # Auth flow (ADR-0009) — additional keys wired as the routes land: # ENTRA_CLIENT_CERT_PATH (alternative to ENTRA_CLIENT_SECRET) # ENTRA_ACCEPTED_TENANT_IDS (CSV; restricts which tenants can sign in # in the multi-tenant phase — empty means # "only ENTRA_TENANT_ID is accepted") # # Sessions (ADR-0010) — additional keys wired as the layers land: # REDIS_SENTINEL_HOSTS (CSV `host:port,host:port,…`; prod HA) # REDIS_SENTINEL_NAME (master name in Sentinel; prod HA) # REDIS_TLS ('true' in prod) # # MFA (ADR-0011): # MFA_FRESHNESS_SECONDS (default 600) # # Audit trail (ADR-0013): # AUDIT_DATABASE_URL (separate creds, role 'audit_writer') # AUDIT_ARCHIVER_DATABASE_URL (role 'audit_archiver', for the retention purge job) # AUDIT_RETENTION_DAYS (default 365) # # Downstream API access (ADR-0014): # OBO_CACHE_ENCRYPTION_KEY (32-byte base64, distinct from SESSION_ENCRYPTION_KEY) # BFF_JWKS_PRIVATE_KEY_PATH # BFF_JWKS_KID # _API_BASE_URL (per integrated downstream) # _TIMEOUT_MS (optional, defaults to 5000)