Configuration
À quoi ça sert
Section intitulée « À quoi ça sert »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
undefinedsournois 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 objetenven argument (défautprocess.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.
Le fichier api/src/config/index.ts
Section intitulée « Le fichier api/src/config/index.ts »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)— retournevaluesi fournie ; sinon, enproduction, lève une erreur (« Refusing to boot with insecure dev fallback ») ; en dev/test, retourne lefallbacknon sécurisé. Utilisé pourCLIENT_TOKEN_SECRETetCOOKIE_SECRET.requireInProdOptional(name, value, nodeEnv)— retournevaluesi fournie ; sinon, enproduction, lève ; en dev/test, retourneundefined. Utilisé pourBOOTSTRAP_SIGNING_KEYetPSP_CONFIG_KEK(la feature reste désactivée en dev plutôt que d’avoir un faux secret).
// api/src/config/index.ts — signature publiqueexport type Config = z.infer<typeof ConfigSchema>export function loadConfig(env: NodeJS.ProcessEnv = process.env): ConfigQui appelle loadConfig ?
Section intitulée « Qui appelle loadConfig ? »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.
Toutes les variables d’environnement
Section intitulée « Toutes les variables d’environnement »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). |
Variables sensibles à NODE_ENV
Section intitulée « Variables sensibles à NODE_ENV »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"]
Comment ça valide concrètement
Section intitulée « Comment ça valide concrètement »loadConfig ne fait pas que copier des valeurs : Zod coerce et contraint.
- Coercition numérique :
PORT=3000(string) devient3000(number) viaz.coerce.number(). - Coercition booléenne :
COOKIE_SECURE=false(string) devientfalse(boolean). - Validation d’URL :
REDIS_URL=pas-une-urllève une erreur Zod (testé). - Longueur minimale :
CLIENT_TOKEN_SECRETetCOOKIE_SECRET< 32 chars sont rejetés avec un message explicite. - Transformation CSV :
CORS_ALLOWED_ORIGINSest 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).
# Lancer les tests de configpnpm --filter api test src/config/index.test.tsGénération des secrets
Section intitulée « Génération des secrets »Les secrets crypto ne s’inventent pas à la main. Le dépôt fournit des scripts (déclarés dans api/package.json) :
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 :
pnpm psp:set-config -- --merchant=<id> --provider=stripe --secret=sk_... --whsec=whsec_...Fichiers liés
Section intitulée « Fichiers liés »| 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 | — |
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Sécurité — pourquoi un seul lecteur de
process.env, gestion des secrets, refus de boot. - Idempotence et Contrats — où s’appliquent
CLIENT_TOKEN_SECRETet les secrets Titreo. - Couche Infrastructure — comment
config.firebird,config.redisetconfig.pspConfigKeksont consommés. - Workers et Couche HTTP — les autres consommateurs de
loadConfig.