Aller au contenu

Configuration

Un backend a besoin de paramètres externes : où est la base de données, l’URL de Redis, les secrets de signature, les clés d’API Titreo, etc. Ces valeurs varient entre le poste du développeur, la CI et la production. On ne veut surtout pas les coder en dur dans le source.

La couche config/ règle ce problème avec une seule règle simple :

Pourquoi cette discipline ?

  • Validation centralisée : Zod vérifie types, formats et longueurs minimales au démarrage. Si une variable est absente ou malformée, le serveur refuse de booter avec un message clair — plutôt qu’un undefined sournois qui casse trois heures plus tard.
  • Sécurité : un secret n’existe qu’à un endroit. On peut auditer d’un coup d’œil quels secrets sont requis en production. Voir Sécurité.
  • Testabilité : loadConfig(env) accepte un objet env en argument (défaut process.env). Les tests passent un faux environnement sans polluer le vrai.
  • Découplage : le domaine ne sait pas que la config vient de variables d’environnement. Il reçoit juste un objet typé. Voir Architecture hexagonale.

Un seul fichier, trois éléments.

Élément Type Rôle
ConfigSchema schéma Zod Décrit la forme finale de la config : types, défauts, contraintes (longueur min, URL valide…).
Config type TS z.infer<typeof ConfigSchema> — le type injecté partout dans l’app.
loadConfig(env?) fonction Lit env (défaut process.env), mappe les noms SCREAMING_SNAKE vers la forme imbriquée du schéma, applique les fallbacks dev, puis ConfigSchema.parse(...). Lève si invalide.

Deux helpers internes gèrent les secrets sensibles aux fallbacks :

  • requireInProd(name, value, fallback, nodeEnv) — retourne value si fournie ; sinon, en production, lève une erreur (« Refusing to boot with insecure dev fallback ») ; en dev/test, retourne le fallback non sécurisé. Utilisé pour CLIENT_TOKEN_SECRET et COOKIE_SECRET.
  • requireInProdOptional(name, value, nodeEnv) — retourne value si fournie ; sinon, en production, lève ; en dev/test, retourne undefined. Utilisé pour BOOTSTRAP_SIGNING_KEY et PSP_CONFIG_KEK (la feature reste désactivée en dev plutôt que d’avoir un faux secret).
// api/src/config/index.ts — signature publique
export type Config = z.infer<typeof ConfigSchema>
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config

Chaque point d’entrée de processus l’appelle une fois au démarrage, puis passe l’objet Config au reste.

api/src/http/server.ts → loadConfig() (serveur Fastify)
api/src/workers/outbox.ts → loadConfig() (worker outbox)
api/src/workers/cleanup.ts → loadConfig() (worker de nettoyage)
api/src/infrastructure/db/migrate-cli.ts → loadConfig() (CLI migrations)

Aucun autre fichier n’appelle loadConfig. (Quelques bootstraps lisent encore directement process.env pour des réglages opérationnels hors schéma — voir l’encadré « La règle d’or » plus haut.) Voir Parcours d’appel pour le flux complet du boot serveur.

Tableau exhaustif, dérivé directement du schéma Zod. La colonne « Requis » indique le comportement quand la variable est absente.

Variable d’env Champ Config Type / contrainte Défaut Requis Rôle
NODE_ENV nodeEnv enum development | test | production development non Pilote les fallbacks et le durcissement (voir lignes ci-dessous).
LOG_LEVEL logLevel enum fatal | error | warn | info | debug | trace info non Verbosité du logger.
PORT port entier > 0 (coercé) 3000 non Port d’écoute HTTP.
HOST host string 0.0.0.0 non Interface d’écoute.
FB_HOST firebird.host string 127.0.0.1 non Hôte Firebird.
FB_PORT firebird.port entier > 0 (coercé) 3050 non Port Firebird.
FB_DATABASE firebird.database string '' (vide) de facto Chemin/alias de la base. Vide = connexion inutilisable ; à fournir en pratique.
FB_USER firebird.user string SYSDBA non Utilisateur Firebird.
FB_PASSWORD firebird.password string '' (vide) de facto Mot de passe Firebird.
REDIS_URL redis.url URL valide redis://127.0.0.1:6379 non File outbox / cache. Rejeté si ce n’est pas une URL.
BOOTSTRAP_SIGNING_KEY bootstrapSigningKey string (PEM Ed25519) undefined prod Clé privée de signature du payload bootstrap servi aux plugins. Absente en dev → feature off.
PSP_CONFIG_KEK pspConfigKek string (base64, 32 octets AES-256) undefined prod KEK qui chiffre/déchiffre les credentials PSP stockés en base. Absente en dev → feature off.
CLIENT_TOKEN_SECRET clientTokenSecret string, ≥ 32 chars fallback dev dev_only_… prod Secret de signature des tokens client (sessions de paiement).
CLIENT_TOKEN_TTL_SECONDS clientTokenTtlSeconds entier > 0 (coercé) 900 non Durée de vie d’un token client (15 min).
COOKIE_SECRET cookieSecret string, ≥ 32 chars fallback dev dev_only_… prod Secret de signature des cookies (Console marchand).
COOKIE_SECURE cookieSecure booléen (coercé) true en prod, sinon false non Flag Secure des cookies. Dérivé de NODE_ENV si non fourni.
TRUSTED_PROXIES trustedProxies string loopback non Proxies de confiance pour X-Forwarded-For (Fastify trustProxy).
CORS_ALLOWED_ORIGINS corsAllowedOrigins CSV → string[] ''[] non Origines autorisées. Chaîne séparée par virgules, trimmée, vides filtrées.
EXPOSE_VERIFICATION_LINK exposeVerificationLink booléen (coercé) false en prod, true sinon non Expose le lien de vérification (debug). Dérivé de NODE_ENV si non fourni.
CONSOLE_BASE_URL consoleBaseUrl URL valide http://localhost:3500 non Base URL de la Console marchand (redirections, liens).
TITREO_API_BASE titreo.baseUrl URL valide conditionnel Base de l’API Titreo (cartes cadeaux).
TITREO_ENSEIGNE titreo.enseigne string, min 1 conditionnel Identifiant enseigne Titreo.
TITREO_API_KEY titreo.apiKey string, min 1 conditionnel Clé d’API Titreo (64 chars fournis par BH-Tech).

Trois comportements dépendent de NODE_ENV === 'production' :

flowchart TD
    A["NODE_ENV"] --> B{"= production ?"}
    B -->|oui| C["CLIENT_TOKEN_SECRET / COOKIE_SECRET absents -> throw (refus de boot)"]
    B -->|oui| D["PSP_CONFIG_KEK / BOOTSTRAP_SIGNING_KEY absents -> throw"]
    B -->|oui| E["COOKIE_SECURE et EXPOSE_VERIFICATION_LINK -> defauts durcis (true / false)"]
    B -->|non| F["Fallbacks dev : secrets dev_only_..., features crypto desactivees, cookies non-secure"]

loadConfig ne fait pas que copier des valeurs : Zod coerce et contraint.

  • Coercition numérique : PORT=3000 (string) devient 3000 (number) via z.coerce.number().
  • Coercition booléenne : COOKIE_SECURE=false (string) devient false (boolean).
  • Validation d’URL : REDIS_URL=pas-une-url lève une erreur Zod (testé).
  • Longueur minimale : CLIENT_TOKEN_SECRET et COOKIE_SECRET < 32 chars sont rejetés avec un message explicite.
  • Transformation CSV : CORS_ALLOWED_ORIGINS est découpé sur ,, trimmé, et les entrées vides sont filtrées → tableau propre.

Ces comportements sont couverts par api/src/config/index.test.ts (env minimal + défauts, rejet d’URL invalide, refus de boot prod sans chaque secret requis, fallbacks dev).

Fenêtre de terminal
# Lancer les tests de config
pnpm --filter api test src/config/index.test.ts

Les secrets crypto ne s’inventent pas à la main. Le dépôt fournit des scripts (déclarés dans api/package.json) :

Fenêtre de terminal
pnpm keygen:bootstrap # -> BOOTSTRAP_SIGNING_KEY (clé privée Ed25519 PEM)
pnpm keygen:kek # -> PSP_CONFIG_KEK (32 octets AES-256 en base64)

Les credentials PSP par marchand (Stripe, PayPlug…) ne passent pas par l’environnement : ils sont stockés chiffrés en base (chiffrés avec la KEK ci-dessus) et provisionnés via CLI :

Fenêtre de terminal
pnpm psp:set-config -- --merchant=<id> --provider=stripe --secret=sk_... --whsec=whsec_...
Fichier Rôle Exports clés
api/src/config/index.ts Schéma + chargement + validation de la config Config (type), loadConfig(env?)
api/src/config/index.test.ts Tests de loadConfig (défauts, coercition, refus de boot prod) — (suite Vitest)
api/.env Environnement local effectif (non commité, chmod 600)
api/.env.docker Variables pour l’exécution conteneurisée
api/.env.example Gabarit documenté des variables attendues
  • Sécurité — pourquoi un seul lecteur de process.env, gestion des secrets, refus de boot.
  • Idempotence et Contrats — où s’appliquent CLIENT_TOKEN_SECRET et les secrets Titreo.
  • Couche Infrastructure — comment config.firebird, config.redis et config.pspConfigKek sont consommés.
  • Workers et Couche HTTP — les autres consommateurs de loadConfig.