Nodefony
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Environnement — variables, cascade des .env, et qui gagne

stablenodefony (cœur)mis à jour 2026-07-25

📍 DocumentationCœur — @nodefony/coreEnvironnement

Une variable d’environnement qui « ne prend pas » est le bug le plus long à comprendre du métier : rien n’échoue, rien ne s’affiche, la valeur est simplement ignorée et un défaut s’applique en silence. Cette page dit où poser une valeur, qui l’emporte, et surtout comment le vérifier au lieu de le supposer.

🧭 Démarrage rapide#

nodefony env          # la cascade, chaque variable, sa valeur EFFECTIVE et sa PROVENANCE
nodefony env --json   # le même rapport, pour un script ou un agent

La commande ne démarre rien (cli/env.ts:208) — elle répond même sur une application qui ne boote plus, ce qui est précisément le moment où on la lance. Elle sort en 78 (EX_CONFIG) si une variable requise manque, pour qu’un script s’arrête là plutôt que de tenter un démarrage voué à l’échec.

Ce qu’elle montre, et qu’aucune lecture de fichier ne donne :

Pourquoi ces deux dernières listes sont séparées. Le framework écrit des NF_* dans l’environnement sans que vous les ayez demandées : le lanceur du CLI marque sa délégation au nodefony du projet (NF_CLI_DELEGATED), les commandes de démarrage inscrivent le mode (NF_MODE_START), le maître de grappe signale la grappe (NF_CLUSTER), la déclaration MCP porte son jeton (NF_MCP_TOKEN). Rangées parmi les inconnues, elles vous accusaient d’une faute de frappe que vous n’aviez pas commise — et proposaient de « corriger » NF_CLI_DELEGATED en NF_ADMIN_PASSWORD. Elles ne sont pas tues pour autant : une variable présente que le rapport passe sous silence vous ferait chercher pourquoi votre environnement ne ressemble pas à ce qu’il montre.

Le modèle : deux axes, une cascade, un seul lecteur#

Trois décisions gouvernent tout ce qui suit, et rien d’autre n’est à retenir.

Deux axes plutôt qu’un : comment le code s’exécute (le mode) et il s’exécute (le déploiement) sont deux questions distinctes — un staging tourne en mode production.

Une cascade, jamais un écrasement : chaque source pose ce que les plus fortes n’ont pas déjà posé. La précédence n’est donc pas une règle appliquée quelque part, c’est une conséquence de l’ordre de lecture — il n’y a rien à synchroniser, et rien qui puisse diverger.

Un seul lecteur de process.env : env.ts. Tout le reste de l’application lit un objet typé, validé au démarrage. Une variable non déclarée là n’existe pas, quoi qu’en dise un fichier .env — c’est ce qui rend une faute de frappe muette, et c’est pourquoi nodefony env existe.

Les deux axes : mode et déploiement#

Nodefony sépare ce que la plupart des frameworks confondent :

Axe Variable Valeurs Ce qu’il décide
Mode NODE_ENV development / production comment le code s’exécute (optimisations, journaux)
Déploiement APP_ENV / NF_ENV chaîne libre : staging, canary il s’exécute (quelle base, quels secrets)

Un staging tourne en mode production : ce sont deux questions différentes, et les mélanger oblige à choisir entre « optimisé » et « pointe la bonne base ». Le déploiement est plus spécifique que le mode, donc plus fort dans la cascade.

Et si NODE_ENV n’est pas posé ?#

C’est le cas de tous les jours sur une machine de développement — et la réponse n’est pas « au hasard ». Poser NODE_ENV est un acte de déploiement ; ne rien poser est l’état d’un poste de développement.

NODE_ENV Mode retenu Pourquoi
development / dev development déclaré
production / prod production déclaré
absent development personne n’a rien dit → poste de développement
posé mais autre (staging, canary, prod-eu…) production un DÉPLOIEMENT est nommé — il tourne comme prod
chaîne vide production choix conservateur : « vidée » ≠ « jamais posée »

Une chaîne vide compte comme posée : on ne distingue pas « vidée par erreur » de « vidée exprès », et se tromper vers la production ne coûte qu’une commande utilitaire, là où l’inverse exposerait la console d’administration d’un serveur.

Le défaut ne gouverne jamais un serveur. nodefony development, nodefony production (alias start, prod) et nodefony cluster posent leur mode eux-mêmes, et il n’existe pas d’autre façon d’en démarrer un. Le défaut ne concerne donc que les commandes utilitaires — inspect, check, env, security:*.

⚠️ Le piège à connaître : une commande utilitaire ne tourne PAS dans le mode du serveur que vous avez lancé. Chacune démarre son propre noyau. Si votre serveur tourne par nodefony development mais que NODE_ENV n’est pas dans votre shell, une commande lancée à côté partira bien en development — mais le jour où vous exportez NODE_ENV=production pour un essai, elle changera de base de données sans rien dire d’autre. Demandez le mode plutôt que de le supposer :

npx nodefony env                 # le mode, et d'où vient chaque variable
npx nodefony inspect routes      # la dernière ligne indique l'environnement

Pour forcer explicitement, préfixez la commande : NODE_ENV=production npx nodefony ….

La cascade — qui gagne#

Du plus fort au plus faible. Le premier niveau qui pose une valeur gagne ; les suivants sont ignorés, sans message.

Rang Source Committé ? Rôle
1 process.env shell, orchestrateur, k8s — gagne toujours
2 .env.<déploiement>.local ❌ non secrets de CE déploiement, sur CETTE machine
3 .env.<mode>.local ❌ non secrets du mode
4 .env.local ❌ non secrets communs, machine du développeur
5 .env.<déploiement> ✅ oui réglages partagés du déploiement
6 .env.<mode> ✅ oui réglages partagés du mode
7 .env ✅ oui défauts communs — le plus faible

Deux règles suffisent à retrouver cet ordre de mémoire : les *.local priment sur les committés, et à rang égal, le plus spécifique gagne.

L’ordre est produit par une fonction unique, envFileOrder (loadEnv.ts:72) — celle-là même que nodefony env affiche : un ordre montré qui différerait de l’ordre appliqué serait pire que pas d’affichage du tout. L’injection (loadEnv.ts:88) n’écrase jamais une clé déjà posée, ce dont toute la précédence découle.

Le chargement a lieu une fois, au démarrage du binaire, avant la construction du noyau : les configurations de modules lisent process.env pendant le boot, il doit donc être peuplé avant elles.

Déclarer une variable — env.ts#

env.ts est le seul endroit du projet qui lit process.env (defineEnv.ts:270). Une variable non déclarée là n’existe pas pour l’application, quoi qu’un fichier .env en dise.

import { defineEnv, envNumber, envEnum, envString } from "nodefony";

export const env = defineEnv({
  NF_PORT: envNumber({ default: 5151, description: "Port HTTP." }),
  NF_LOG_DRIVER: envEnum(["stdout", "file", "null"], {
    default: "stdout",
    description: "Destination des journaux.",
  }),
  NF_DATABASE_URL: envString({
    optional: true,
    description: "URL de la base.",
  }),
});

Ce que la déclaration apporte, et qu’une lecture directe de process.env ne donne pas : la valeur est typée (number, boolean, énumération), validée au démarrage (une valeur hors énumération échoue tout de suite, avec le nom de la variable), documentée (description alimente .env.example et nodefony env), et atteignable typée dans la configuration via ctx.env.

Une variable requise est celle qui n’a ni défaut ni optional: true. nodefony env les nomme, et sort en erreur si l’une manque.

Requise LÀ-BAS seulement — requiredIn#

Certaines variables ne sont indispensables qu’en production. Les déclarer optional est vrai sur le poste du développeur et faux là où ça compte ; les déclarer requises empêcherait de démarrer en local. requiredIn nomme les environnements où l’absence devient une faute :

NF_CSRF_SECRET: envString({
  optional: true,
  requiredIn: ["production"],
  description: "Secret des jetons anti-CSRF — partagé entre process en cluster.",
}),

Le cas qui fonde cette règle n’est pas un secret laissé en dur, c’est l’inverse : un secret absent est engendré à la volée. Rien ne va mal au premier démarrage — c’est au deuxième exemplaire que les jetons émis par l’un se font refuser par l’autre, sans une ligne dans les journaux.

Les noms sont libres : ils se comparent aux étiquettes de l’environnement courant, à savoir le mode d’exécution (NODE_ENV) et l’environnement de déploiement (NF_ENV, ou l’alias de plateforme APP_ENV) quand il en diffère. Une préproduction qui tourne en production porte donc les deux, et requiredIn: ["preprod"] y mord.

Trois lecteurs appliquent la MÊME règle (isEnvVarRequired) : le démarrage refuse de partir, nodefony env marque la variable, et nodefony doctor la nomme avant qu’on déploie.

Demander ce qui manquera ailleurs — --env#

nodefony doctor --env production et nodefony env --env production évaluent les exigences pour l’environnement visé, avec les valeurs présentes ici : on ne simule pas un déploiement, on demande ce qui manquera là-bas. Le rapport l’annonce en tête, et sort en erreur si une variable requise à destination n’a aucune valeur.

doctor signale aussi un fichier .env*.local suivi par git — l’historique garde les secrets même après suppression. Sans dépôt git, il ne conclut pas : il énonce le contrôle comme non fait.

Secrets : <VARIABLE>_FILE#

Un secret monté par Docker ou Kubernetes est un fichier, pas une valeur. Toute variable accepte donc la forme <VARIABLE>_FILE, qui pointe le fichier à lire :

NF_TOTP_KEY_FILE=/run/secrets/totp_key    # au lieu de NF_TOTP_KEY=…

Poser les deux échoue au démarrage : entre deux sources contradictoires, deviner serait le pire service. Les valeurs des variables dont le nom évoque un secret ne sont jamais rendues en clair par nodefony env — seulement leur présence, leur longueur et leur provenance.

Surcharger une clé de module — NF__#

Deux mécanismes coexistent, et les confondre est l’erreur la plus fréquente :

Forme Ce que c’est Où c’est déclaré
NF_PORT=5151 variable de l’application, typée et validée env.ts — non déclarée = sans effet
NF__HTTP__SERVERS__HTTPS__PORT=8443 surcharge directe d’une clé de module rien à déclarer — __ sépare les segments

Le second (envOverride.ts:80) vise une clé de configuration par son chemin, sans passer par env.ts : NF__<MODULE>__<CHEMIN…>. Une liste s’écrit en valeurs séparées par des virgules. Un segment mal orthographié est signalé au démarrage avec la clé la plus proche — le « vouliez-vous dire » de git.

Réserve ce mécanisme à ce qu’il fait bien : régler une brique en exploitation sans toucher au code. Ce que l’application possède en propre se déclare dans env.ts.

🧪 Tests#

Le calcul du rapport est un module pur (envReport.ts:177) : il reçoit la cascade déjà lue et l’environnement effectif, et conclut. Cette séparation est ce qui rend éprouvables les trois affirmations sur lesquelles on va se fier pour corriger une configuration — d’où vient une valeur, ce qui est masqué, ce qui n’a aucun effet. Se tromper sur l’une d’elles est pire que de ne rien afficher : on croit le rapport, et on cherche ailleurs.

⚠️ Pièges#

📖 Lexique#

🔗 Pour aller plus loin#