---
title: "Configurer une application Nodefony (defineConfig)"
source: docs/guides/configuration.md
url: https://nodefony.github.io/nodefony-core/docs/guides/configuration/
---


> Modèle `defineConfig` (depuis 2026-06-05). La config d'une app tient dans **un fichier
> racine** auto-documenté, et grandit par **composition** — sans jamais subir le découpage.

📍 [Documentation](../../) › [Guides](../) › **Configuration**

## Le modèle — un fichier racine, qui grandit par composition

**Commencer minuscule comme Vite. Pouvoir grandir structuré. Sans jamais subir le découpage.**

Tout ce que vous n'écrivez pas prend le **défaut du framework** (`defaultAppConfig`, dans le core).
Votre `nodefony.config.ts` ne contient donc QUE vos écarts.

## Vue d'ensemble — comprendre TOUTE la config en 1 minute

La config se lit sur **quatre couches**, classées par une **échelle de précédence unique** (le plus bas perd, le plus haut gagne) :

| #   | Couche                 | Qui décide | Où                                                                                 |
| --- | ---------------------- | ---------- | ---------------------------------------------------------------------------------- |
| 1   | **Défaut** (framework) | Nodefony   | schéma Zod du core (`defaultAppConfig`) + de chaque module                         |
| 2   | **Config du projet**   | vous       | `nodefony.config.ts` (vos écarts, par-env via `ctx` ; inclut `ctx.env` = `env.ts`) |
| 3   | **Déploiement**        | le devops  | variables d'env — catalogue `NF_X` + override générique `NF__*` + secrets `*_FILE` |
| 4   | **Invocation**         | la CLI     | flags (`--workers`, …)                                                             |

> **La seule règle à retenir** : ce que vous n'écrivez pas prend le **défaut du framework**. Vous
> n'écrivez que vos **écarts** dans `nodefony.config.ts`. Le déploiement (Docker/k8s) surcharge par
> **variable d'environnement**, sans toucher au code. Studio affiche la **provenance** de chaque valeur.

Décision d'architecture complète : [ADR-0006](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/0006-configuration-unifiee-env-override.md).

## Les deux fichiers racine

| Fichier              | Rôle                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `nodefony.config.ts` | La config : `defineConfig((ctx) => …)` + manifeste `modules`                             |
| `env.ts`             | Le catalogue des variables d'environnement (`defineEnv`) — SEUL lecteur de `process.env` |

`index.ts` (racine) importe le descripteur et ré-exporte `env` :

```typescript
import config from "./nodefony.config";
export { env } from "./env";
// … le Module App reçoit `config` via super(...)
```

## `nodefony.config.ts` — l'orchestrateur

```typescript
import { defineConfig, use } from "nodefony";
import type { env } from "./env";

export default defineConfig<typeof env>((ctx) => ({
  domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1", // par-env via ctx
  log: { debug: ctx.isProd ? [] : "*", driver: ctx.env.NF_LOG_DRIVER },
  modules: [
    use(
      "@nodefony/http",
      { trustedHosts: ["localhost"] },
      { policy: "mandatory" },
    ),
    "@nodefony/framework",
    { name: "@nodefony/test", policy: "dev" },
  ],
}));
```

`ctx` est passé au boot : `{ env, appEnv, runtimeEnv, isProd, isDev, isTest }`.

- `env` = le catalogue typé de `env.ts` (`ctx.env.NF_LOG_DRIVER` est auto-complété + documenté en hover).
- `runtimeEnv` = `NODE_ENV` canonisé ; `appEnv` = axe de déploiement libre (`APP_ENV`/`NF_ENV`).

## `env.ts` — le catalogue d'environnement

```typescript
import { defineEnv, envEnum, envBoolean, envString } from "nodefony";

export const env = defineEnv({
  NF_LOG_DRIVER: envEnum(["stdout", "file", "null"] as const, {
    default: "stdout",
  }),
  NF_LOG_FILE_SYNC: envBoolean({ default: false }),
  NF_LOKI_URL: envString({ optional: true }),
});
```

- Une variable **présente mais invalide** (enum hors liste, nombre malformé, requise manquante)
  fait **échouer le boot** avec un message clair nommant la variable — pas de fallback silencieux.
- Une variable **absente** prend le défaut déclaré.
- ⚠️ **`as const`** sur les valeurs d'`envEnum([...])` : sinon l'union littérale (`"stdout" | "file" | "null"`)
  est élargie en `string`, et un champ qui attend l'union (ex. `log.driver`) ne typecheck plus.

## Les 6 recettes (faire grandir la config)

1. **Ajouter un module** → ajouter son nom dans `modules`.
2. **Configurer un module** → `use("@nodefony/security", { firewalls: { … } })`.
3. **Module dev/conditionnel** → `{ name, policy: "dev" }` ou `use(name, config, { when: (c) => … })`.
4. **Réglage par-env** → tester `ctx.isProd` / `ctx.isDev` dans la fonction.
5. **Lire une variable d'env** → la déclarer dans `env.ts`, lire `ctx.env.X` (jamais `process.env`).
6. **Configurer un module** → un fichier `nodefony/config/<module>.ts` que le manifeste importe (voir juste en dessous). C'est la forme NORMALE, pas un recours quand un bloc grossit.

### Où vit la config d'un module — l'emplacement, et la garde qui vient avec

Le manifeste reste l'**index ordonné** : quels modules, dans quel ordre, sous quelle politique.
C'est la seule chose sur laquelle le Kernel décide, et la seule qu'un outil du produit y écrit. La
configuration d'un module, elle, part dans son fichier.

**Deux choses à ne pas confondre — l'une est gardée, l'autre est une convention.**

- **Ce qu'un automate garde** : _si_ tu extrais, tu écris `satisfies`. `nodefony doctor` refuse un
  fragment qui s'en passe (`fragment-without-satisfies`). C'est vérifiable sans jugement, donc
  c'est une règle.
- **Ce qui relève de la convention** : _quand_ extraire. Une application qui naît reçoit ses deux
  gros blocs déjà extraits — c'est l'exemple sous les yeux —, et laisse en ligne un réglage d'une
  clé. Ce dépôt, lui, applique la forme à **tous** ses modules configurés : il est mature, et la
  prévisibilité y vaut plus que la concision.

Aucun automate ne peut dire « tu aurais dû extraire ce bloc » : le seuil serait arbitraire et se
rejugerait à chaque édition. Prétendre le contraire donnerait une règle que rien ne fait respecter,
c'est-à-dire pas une règle.

L'emplacement est **`nodefony/config/<module>.ts`**, jamais un dossier `config/` à la racine :
tout le code d'une application vit déjà sous `nodefony/` (`controllers/`, `entity/`, `security/`),
ce dossier est dans l'`include` des deux `tsconfig` et dans le glob du bundler. Un fichier oublié
y reste au moins typechecké, quand un fichier racine serait invisible tant que personne ne
l'importe.

**Le nom est réservé, et la réserve ne crie pas.** `config.ts` et `*.config.ts` appartiennent au
chargement d'un **module** (`<module>/nodefony/config/config.ts`, produit par `nodefony create
module` et importé par l'`index.ts` du module) : dans `<app>/nodefony/config/`, les lecteurs du
manifeste les **écartent** — sinon on ne saurait plus dire, à la lecture, si un fichier est chargé
seul par le framework ou parce que le manifeste l'importe. Un fragment se nomme donc
**`<module>.ts`** (`security.ts`, jamais `security.config.ts`) : mal nommé, il n'est lu par aucun
contrôle **et chargé par personne** — `nodefony doctor` le signale (`reserved-fragment-name`).
Seul le sous-dossier `cluster/` fait exception : il est lu par chemin par le process maître.

> ⚠️ **Extraire SANS `satisfies` désarme le typage**, et le désarme en silence. Le contrôle des
> propriétés en excès de TypeScript ne porte que sur un **littéral écrit au point où le type est
> attendu** : dès que le bloc part dans une fonction, une clé mal orthographiée compile — et la faute
> n'apparaît qu'au démarrage, quand le schéma strict du module la refuse : tard, et sans la ligne de
> code fautive. Aucun helper ne peut
> rattraper ça : le typage contextuel du retour ne déclenche jamais le contrôle. **Seul `satisfies`
> sur le littéral le fait.**
>
> C'est pourquoi `nodefony doctor` **refuse** un fragment qui rend une configuration sans
> `satisfies` (constat `fragment-without-satisfies`) : extraire un bloc ne rend pas l'application
> plus sûre, cela lui RETIRE une garde — et `satisfies` est ce qui la rend.

```typescript
// nodefony/config/http.ts
// Le type d'ENTRÉE vient du MODULE, jamais du cœur : `nodefony` n'exporte pas
// `IHttpConfigInput`.
import type { ConfigContext } from "nodefony";
import type { IHttpConfigInput } from "@nodefony/http";
import type { env } from "../../env";

export const httpConfig = (ctx: ConfigContext<typeof env>) =>
  ({
    headerServer: ctx.isProd ? null : "nodefony",
    upload: { maxFileSize: 1_048_576 },
  }) satisfies IHttpConfigInput;
```

```typescript
// nodefony.config.ts — garde les use(), qui restent l'index ordonné des modules
import { defineConfig, use } from "nodefony";
import { httpConfig } from "./nodefony/config/http";

export default defineConfig((ctx) => ({
  modules: [use("@nodefony/http", httpConfig(ctx))],
}));
```

Ce que le `satisfies` attrape, vérifié sur ce dépôt :

```typescript
// ❌ TS2561 — « trustProxi n'existe pas… vouliez-vous écrire trustProxy ? »
//    Sans `satisfies`, ce MÊME fichier compile ; la faute n'apparaît qu'au boot (BootConfigurationError).
trustProxi: true;

// ❌ TS2353 — `servers` appartient à la config de l'APPLICATION, pas au module.
//    La confusion des deux niveaux est la faute la plus facile à faire ici.
servers: {
  http: {
    port: 5151;
  }
}
```

Rien ne charge `nodefony/config/` tout seul : c'est un import ordinaire, et le fichier ne vaut que
parce que `nodefony.config.ts` le nomme.

**La fonction ne prend `ctx` que si elle s'en sert** : une configuration qui ne change pas avec
l'environnement s'écrit `() => ({ … }) satisfies …`, et le manifeste l'appelle sans argument. Ce
n'est pas un détail de style — `noUnusedParameters` refuse un paramètre inutilisé, et la signature
dit ainsi, à la lecture, si ce bloc dépend ou non de l'environnement.

**Ce dépôt s'applique la forme à lui-même** : il est une application Nodefony, et ses sept modules
configurés portent chacun leur fragment sous [`nodefony/config/`](https://github.com/nodefony/nodefony-core/tree/main/nodefony/config) —
`http.ts`, `framework.ts`, `realtime.ts`, `security.ts`, `studio.ts`, `devkit.ts`, `drizzle.ts`. Son manifeste
est passé de 570 à 215 lignes sans qu'aucune valeur effective ne bouge : `nodefony inspect config
--json` rend le même objet, au caractère près, avant et après.

Une application générée reçoit la même forme, sur les blocs qui la méritent :
`nodefony/config/devkit.ts` partout, et avec le preset complet `nodefony/config/security.ts` et
`nodefony/config/drizzle.ts` (ou `mongoose.ts` sur MongoDB).

## L'écoute : ports et TLS

`servers` ne porte que des **écarts** ; laissé vide, le framework écoute en 5151 (HTTP) et 5152
(HTTPS, en HTTP/2).

**Un port appartient au DÉPLOIEMENT, pas au code.** En PaaS (Cloud Run, Heroku, Railway) la
plateforme IMPOSE le sien via `PORT` : un port écrit en dur donne un service qui écoute là où
personne n'appelle. D'où la lecture par l'environnement (`NF_PORT`, `NF_PORT_HTTPS`, `PORT`).
En développement, deux applications Nodefony peuvent tourner côte à côte : un port déjà pris
**glisse au suivant** et le décalage est ANNONCÉ (`portPolicy: "auto"`, défaut hors production).
En production et en test, `portPolicy: "strict"` → échec franc plutôt que port surprise.

**HTTPS est actif par défaut, même en développement.** Ce n'est pas du zèle : les API navigateur
modernes exigent un contexte sécurisé — WebRTC/`getUserMedia`, presse-papiers, service workers,
notifications. Un projet démarré en clair découvre le problème le jour où il ajoute la première de
ces fonctionnalités.

Au premier boot, un certificat de développement est généré tout seul : via **mkcert** s'il est
installé (autorité locale de confiance, zéro avertissement navigateur), sinon auto-signé.
Inspection et regénération : `npx nodefony http:certificates`.

En production, deux voies : fournir un vrai certificat, ou **terminer le TLS à l'ingress / au load
balancer** et n'exposer qu'un port en clair — `https: false` dans `servers` (HTTPS et WSS en
héritent tous deux). C'est le cas nominal en cloud.

## La console Studio en production

Studio est déclaré `policy: "mandatory"` par le scaffold : la console d'**administration**
(introspection de la config, des sessions, des logs) existe aussi en production, là où l'application
tourne. Un `policy: "dev"` la ferait disparaître de la production sans un mot.

Ce qui est clos : le data plane (`/nodefony/<module>/api/…`) exige une session, par la zone
`nodefony-admin` que pose `@nodefony/framework`. Ce qui ne l'est pas encore : la page elle-même, qu'un
anonyme peut afficher (tous ses appels reçoivent 401). Pour la fermer, ajoutez une zone dans
`nodefony/config/security.ts` : `nodefonyUi: { pattern: "^/nodefony(/|$)", authenticators:
["session"] }` — le pattern le plus long gagne, le data plane garde la sienne.

La molette `ui` décide de la livraison de l'interface : `"static"` (épinglé par le scaffold) sert
les assets pré-buildés du paquet npm — Studio marche sans rien recompiler. `"auto"` / `"vite"`
feraient passer l'UI Studio (React) par **ton** serveur Vite : utile uniquement pour développer
Studio lui-même, et cela exigerait ses plugins dans **tes** devDependencies — une application
Vue ou Angular n'a pas `@vitejs/plugin-react`.

## Le manifeste `modules`

L'ordre du tableau = **ordre (priorité) de chargement**. Trois formes :

| Forme                                 | Signification                               |
| ------------------------------------- | ------------------------------------------- |
| `"@nodefony/http"`                    | string nue = `{ name, policy: "optional" }` |
| `{ name, policy, when }`              | entrée détaillée (gating)                   |
| `use(name, config, { policy, when })` | entrée + **config colocalisée** du module   |

Policies : `mandatory` (socle, jamais gaté) · `optional` (défaut, gaté par `when`) · `dev` (chargé hors production).
`when(config)` reçoit la config résolue ; `false` → module non chargé (0 coût — en ESM un module non importé n'existe pas).

> Le manifeste **remplace** le décorateur `@modules` (retiré 2026-06-03) et les clés `module-<name>` à la racine.

### Quand un MODULE configure un autre module — la précédence

Un module peut porter, dans sa propre configuration, une clé `module-<nom>` adressée à un autre
module (`@nodefony/framework` règle ainsi `@nodefony/security`). Elle est appliquée **après** la
configuration de l'application, donc **c'est le module qui gagne** — y compris un module de banc
(`policy: "dev"`). La règle : un module doit **compléter**, jamais **contredire**. Compléter est
silencieux ; écraser une feuille que l'application a posée par `use()` est signalé en **WARNING**,
avec le module, le chemin, la valeur remplacée et la nouvelle (un secret est rédigé). Le WARNING
entre au bilan de boot : une décision explicite de l'application écrasée par une fixture ne se
manifesterait autrement que par un comportement inexplicable.

⚠️ **Une liste ne se remplace pas : elle se fusionne case par case.** La fusion profonde
(`extend(true, …)`, la même pour la configuration de l'application et pour ces surcharges) traite
un tableau comme un objet indexé :

```typescript
// application                       // module (module-security)
grantableScopes: {                   grantableScopes: {
  "x:write": ["ROLE_A", "ROLE_B"],     "x:write": ["ROLE_C"],
}                                    }
// résultat : "x:write": ["ROLE_C", "ROLE_B"] — ROLE_B survit sans que personne ne l'ait écrit
```

Pour une liste de rôles, d'origines ou d'émetteurs, c'est un droit hérité en silence. La règle qui
l'évite : **une liste n'est déclarée qu'à un endroit** — un module complète par des clés qui sont
les siennes (son préfixe de scopes, sa zone), il ne redéclare pas celles de l'application. Ce
comportement est figé par un test (`src/nodefony/src/tests/Tools.test.ts`).

## Typage par module — `use()` propose les bonnes clés

Pour que `use("@nodefony/x", …)` auto-complète les clés du module x, **le module augmente le registre**
du core (declaration merging, pattern Nuxt/Pinia) :

```typescript
// dans @nodefony/x
declare module "nodefony" {
  interface NodefonyModuleConfig {
    "@nodefony/x": IXConfigInput; // ⚠️ le type d'ENTRÉE, pas celui de sortie
  }
}
```

⚠️ **Le type d'ENTRÉE** (`z.input` du schéma — tout optionnel), **jamais celui de sortie**
(`z.infer`) : après application des défauts, les champs sont requis. Surcharger une seule clé
obligerait alors l'app à réécrire toute la configuration du module.

**Ce que l'augmentation évite vraiment.** Sans elle, `use()` accepte `Record<string, unknown>` : une
clé **mal orthographiée compile**, puis le schéma strict du module la refuse au démarrage. La faute
est donc attrapée, mais tard, sur la machine qui démarre, et sans la ligne de code fautive.
L'augmentation l'avance à la compilation :

```typescript
use("@nodefony/redis", { enabledd: false });
//                       ^^^^^^^^ erreur de compilation, au lieu d'un silence au boot
```

Un module tiers qui ne l'applique pas reste **fonctionnel** (jamais bloquant) — il perd simplement ce
filet. **Convention** : tout module qui expose une config publie son type d'entrée et augmente ce
registre. `nodefony create module` le génère déjà : un module neuf naît conforme.

## Réactivité : `hot` vs `boot`

La réactivité n'est **pas** déduite d'un tag posé sur chaque champ : elle vient d'une **liste
explicite**, `configReactivity` (`src/nodefony/src/config/reactivity.ts#configReactivity`), interrogée par
`getConfigReactivity(path)`.

- **`hot`** — applicable à chaud. Aujourd'hui **trois chemins exactement** : `log.active`,
  `log.debug`, `log.requestFormat` (le cadre de la « fenêtre d'audit » : élever la verbosité en
  production, sans redémarrer).
- **`boot`** — figé au boot, un changement exige un redémarrage : **tout le reste**, ports,
  `protocol`, liste `modules`, `domain` compris.

Un champ absent de la liste est donc `boot` — c'est le défaut sûr. L'application à chaud se fait par
le data plane `PATCH /nodefony/kernel/api/config/{module}` (`KernelAdminApi.ts#createKernelAdminApi`), qu'utilise
l'onglet **Configuration** de Studio, lequel badge chaque champ `🔥 à chaud` / `🔒 redémarrage`.

## Figée au démarrage — et surchargée par requête

**La configuration d'un module est figée à la fin de `onReady`**, avant que le premier serveur
n'écoute. Elle est partagée par toutes les requêtes : l'écrire (`this.options.upload.maxFiles = 3`)
lève désormais une `TypeError` au lieu de changer, en silence, la configuration des requêtes
concurrentes. Une configuration se complète **au démarrage**, jamais après. L'édition à chaud
ci-dessus reste possible : elle remplace la configuration au lieu de l'écrire.

**Une requête peut pourtant porter SA variante** — le cas typique : une application qui sert
plusieurs organisations, chacune avec son quota d'envoi. Elle pose un **calque** : quelques clés
surchargées, pour elle seule, sans jamais toucher la configuration du module.

```ts
// Dans un module de l'application — reconnaître l'organisation, poser son calque.
import { Module, overlayConfig } from "nodefony";

export class TenantModule extends Module {
  override async onKernelBoot(): Promise<this> {
    this.kernel?.on("onRequestScope", (context: unknown) => {
      const { request } = context as {
        request?: { headers?: Record<string, unknown> };
      };
      const host = String(request?.headers?.host ?? "");
      if (host.startsWith("premium.")) {
        overlayConfig("@nodefony/http", {
          maxBodySize: 50_000_000,
          upload: { maxFileSize: 500_000_000 },
        });
      }
    });
    return this;
  }
}
```

Le code qui applique la limite lit la configuration **par `useConfig()`**, qui voit le calque de
la requête courante (et, sans calque, l'objet figé du module, sans rien allouer) :

```ts
import { useConfig } from "nodefony";

const { maxBodySize } = useConfig("@nodefony/http");
```

Trois règles à connaître :

- **Seules les clés de la LISTE BLANCHE du module se surchargent.** Un module qui n'en déclare pas
  n'accepte aucun calque ; une clé hors liste est refusée en la nommant. Aujourd'hui, seul
  `@nodefony/http` en déclare une : `maxBodySize` et les limites `upload.*` (hors `uploadDir`).
  Secrets, pare-feu, en-têtes de sécurité, sessions, limites de débit : jamais. Le tableau complet,
  module par module et avec ses raisons, est dans l'[ADR-0012](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/0012-calque-configuration-par-requete.md).
- **`onRequestScope` est le bon moment** : le premier point où la requête a son contexte, AVANT la
  lecture du corps. Un calque posé plus tard — au pare-feu, dans un contrôleur — arrive après
  l'application des quotas de corps.
- **En WebSocket, le calque suit la connexion** : il vaut pour tous les messages de la socket.

Un module qui ouvre une clé à la surcharge la déclare dans `overlaySchema` (un schéma Zod strict,
bâti avec `overlayField()` pour ne pas hériter des défauts), l'enregistre dans le registre typé
`NodefonyModuleOverlay`, et la lit par `useConfig()` au moment de s'en servir.

## Cas particulier : la topologie cluster

`nodefony/config/cluster/cluster.config.ts` reste un **fichier séparé, kernel-free** : le process master
le lit **standalone, AVANT de booter le moindre Kernel**, pour décider du nombre de workers. Ne PAS le
mettre dans `nodefony.config.ts`. Override runtime : CLI `--workers` > `NF_WORKERS` > ce fichier.

`nodefony create app` **ne génère pas ce fichier** : sa valeur serait le défaut (`workers: 1`, un
process par pod), et chaque application porterait un fichier qui ne dit rien. Les deux voies
d'exécution suffisent à un déploiement ; le fichier ne sert que si la topologie doit vivre en git —
trois lignes, kernel-free, dont le README généré donne la forme. `NF_WORKERS` est en revanche
déclarée dans l'`env.ts` généré, avec ses trois formes (`1`, `"auto"`, `<n>`) : c'est là qu'on
cherche une variable, et c'est ce que `npx nodefony env` cataloguera.

## Quand la config est invalide

Le boot **échoue proprement** (il ne peut pas deviner vos ports/modules) : un diagnostic clair
(titre + cause + champ Zod nommé + **valeurs par défaut du framework explicitées**), **sans stack brute**,
et un code de sortie **`EX_CONFIG` (78)** pour que l'orchestrateur distingue « mauvaise config » d'un crash.

```
✖ Configuration de l'application invalide
  La résolution de la configuration (`defineConfig`) a échoué.
  Cause : servers.http.port: Expected number, received string
  …
  Configuration PAR DÉFAUT du framework (appliquée à tout champ omis) :
    • servers = {"statics":true,"http":{"port":5151}, …}
```

## En développement : rebuild après une modif de config

Le boot lit la config depuis le **dist** (`dist/index.js`, `dist/nodefony.config.js`, `dist/env.js`),
pas la source. Après avoir édité `nodefony.config.ts` / `env.ts`, **rebuilder le root** avant de relancer :
`npm run build` à la racine (le `start.sh` du skill ne rebuilde que le module test en dev).
Voir le skill `nodefony-start-server`.

## Voir la config — l'ÉTAT, et le CATALOGUE

Deux questions différentes, deux réponses :

| La question                                                       | La commande                            |
| ----------------------------------------------------------------- | -------------------------------------- |
| « Qu'est-ce qui est POSÉ aujourd'hui, et d'où ça vient ? »        | `npx nodefony inspect config`          |
| « Qu'est-ce que j'ai le DROIT d'écrire, et que fait cette clé ? » | `npx nodefony inspect schema <module>` |

```bash
npx nodefony inspect schema http          # les clés de @nodefony/http, une fiche par réglage
npx nodefony inspect schema http --json   # le même catalogue, filtrable au `jq`
npx nodefony inspect schema              # tous les modules montés
```

Le catalogue rend, par clé : le **chemin pointé** tel qu'on l'écrit dans `use()`, le **type** (ou
les valeurs de l'énumération), le **défaut** du schéma, la **valeur effective**, sa **provenance**
(`default` / `app` / `env` / `runtime`) et la **description** écrite dans le schéma. C'est cette
dernière qui compte : les schémas du framework portent des centaines de `.describe()` — ce que la
clé fait, ce qui arrive si on l'omet, quel RFC elle applique — et elles n'étaient lisibles qu'en
ouvrant le schéma Zod dans `node_modules`.

La colonne `note` dit ce qui rend une clé particulière : `réservé` (acceptée par le schéma, mais le
code n'en fait encore rien), `secret` (jamais rendue en clair), `dérivé du kernel` (remplie au boot
si on la laisse vide), `modifiable à chaud`.

Un module inconnu est **refusé en nommant** les modules acceptés — une faute de frappe ne rend donc
jamais un catalogue vide, qui se lirait « ce module n'a pas de configuration ».

Ces commandes bootent l'application **sans ouvrir un seul port** : elles cohabitent avec un serveur
de développement en marche. Même donnée côté web : l'onglet **Configuration** de Studio
(`/nodefony/config`), alimenté par `GET /nodefony/kernel/api/config` et
`GET /nodefony/kernel/api/config/schema` — même code, deux portes.

> Il n'existe **pas** de commande `nodefony config:show` : c'est `inspect` qui porte les deux
> lectures.

## Surcharger en déploiement — Docker / variables d'env

### Pour commencer — régler un module sans ouvrir son code

Un module, c'est un appareil livré avec ses réglages d'usine : Redis se connecte à `localhost`,
sur le port `6379`. Tu ne modifies **jamais** les fichiers du module pour changer ça. Tu as trois
boutons, de l'extérieur :

| Bouton                    | Exemple — l'hôte Redis                                                                                       | Pour qui, quand                                                  |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| **1. Le code de l'app**   | `use("@nodefony/redis", { globalOptions: { socket: { host: "redis.interne" } } })` dans `nodefony.config.ts` | le développeur — ce qui ne change pas d'un déploiement à l'autre |
| **2. Le nom court `NF_`** | `NF_REDIS_HOST=redis.interne`                                                                                | l'exploitant — le contrat de PRODUCTION                          |
| **3. La forme `NF__`**    | `NF__REDIS__GLOBALOPTIONS__SOCKET__HOST=redis.interne`                                                       | les bancs, le diagnostic, le dépannage ponctuel                  |

**Le nom court** (`NF_REDIS_HOST`) n'existe que si l'auteur du module l'a prévu. C'est un nom
**stable et documenté** : l'auteur s'engage à ne pas le casser. C'est celui qu'on pose dans un
`.env`, un Secret Kubernetes, les valeurs Helm, l'`environment:` d'un `compose.yaml`. Sa valeur est
validée comme celle du fichier : fausse, elle **arrête le démarrage** en nommant la variable
(`… — valeur posée par NF_REDIS_PORT`). `npx nodefony env` liste ceux des modules de l'application,
et signale ceux d'un module chargé seulement sous condition (`when`) ou en développement : posée,
la variable ne règle rien tant que le module n'est pas chargé.

**La forme `NF__`** marche pour **n'importe quelle** clé de **n'importe quel** module, sans que
personne ne la déclare : chaque `__` descend d'un niveau dans la configuration. C'est sa force, et
c'est aussi sa limite : son nom suit la structure INTERNE du module. Si l'auteur renomme
`globalOptions`, la variable ne vise plus rien. Le démarrage le signale, mais le réglage est perdu.
D'où la règle : **en production durable, le nom court ; la forme `NF__` pour faire varier un
réglage dans un banc, ou pour dépanner tout de suite**. Une équipe qui pose une `NF__` en
production depuis des mois a trouvé un réglage qui mérite un nom court : demande-le au module.

**Un secret se monte en fichier.** En production, un mot de passe n'est pas écrit dans une
variable mais monté par l'orchestrateur (Docker secrets, Kubernetes). Ajoute `_FILE` au nom court :

```bash
NF_REDIS_PASSWORD_FILE=/run/secrets/redis-password   # le contenu du fichier devient la valeur
```

Poser `NF_REDIS_PASSWORD` **et** `NF_REDIS_PASSWORD_FILE` arrête le démarrage : deux valeurs pour
un seul secret, c'est une erreur, pas un choix.

**Nom court ET `NF__` sur le même réglage** (`NF_REDIS_HOST` et
`NF__REDIS__GLOBALOPTIONS__SOCKET__HOST`) : deux valeurs pour un seul réglage.

| Mode          | Ce qui se passe                                                                |
| ------------- | ------------------------------------------------------------------------------ |
| production    | démarrage **refusé** (code 78), le message nomme les deux variables            |
| développement | `NF__` **l'emporte**, et un avertissement le dit en nommant les deux variables |

Pourquoi deux règles : en production, on ne devine pas laquelle l'exploitant voulait. En
développement, c'est le geste normal d'un banc — le `.env` pose le nom court, la mesure fait varier
le réglage en `NF__` le temps d'un essai.

**Une valeur fausse arrête le démarrage**, en nommant la variable : `NF_REDIS_PORT=abc` est refusé,
jamais ignoré. Une variable **vide** (`NF_REDIS_HOST=` dans un `.env`) vaut absente.

**Voir ce qui est réellement appliqué** : `npx nodefony env`. La commande ne démarre pas
l'application, elle répond donc même quand celle-ci ne démarre plus. Elle range chaque variable
selon d'où elle vient (terminal, `.env`), montre les noms courts que proposent tes modules (posés ou
non, avec leur défaut), et signale en dernier ce que personne ne lit — une faute de frappe, le plus
souvent. `--json` rend le même rapport pour un script.

#### Côté auteur de module — donner un nom court à un réglage

Un module a toujours **deux** fichiers de configuration, créés par `nodefony create module` :

| Fichier                                 | Rôle                                                                        |
| --------------------------------------- | --------------------------------------------------------------------------- |
| `nodefony/config/config.ts`             | le schéma : quels réglages existent, leurs défauts — LE fichier qu'on édite |
| `nodefony/config/defineModuleConfig.ts` | la machinerie (valide, gèle) — on n'y touche presque jamais                 |

Donner un nom court à un réglage tient en **une** déclaration, sur le champ, dans `config.ts` :

```ts
host: z.string().min(1).default("localhost")
  .describe("Hôte du serveur Redis.")
  .meta({ env: "NF_REDIS_HOST" }),   // ← le seul geste
```

Rien d'autre à écrire : le démarrage applique la variable (`applyEnvShortcuts()`,
`src/nodefony/src/config/envShortcuts.ts#applyEnvShortcuts`), `nodefony env` la reconnaît et `.env.example` la
décrit, avec la description et le défaut du schéma. `.meta()` se place **en dernier** dans la
chaîne. Le nom suit la forme `NF_<MODULE>_<RÉGLAGE>`.

> ✅ **Livré** ([ADR-0006](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/0006-configuration-unifiee-env-override.md)). Catalogue `env.ts`, override générique `NF__*` et secrets `*_FILE` sont opérationnels : résolus **1× au boot**, validés par le schéma Zod du module (chemin/valeur invalide → boot rejeté, jamais de surcharge silencieuse).

Deux façons d'agir par l'environnement, rôles **distincts** :

- **Catalogue** (`env.ts`, forme `NF_X`) — secrets, choix structurants, défauts à logique, exposés
  **typés** dans `ctx.env`. Expérience **développeur**.
- **Override générique** (forme `NF__<MODULE>__<CHEMIN>`) — surcharge de **n'importe quel** champ d'un
  module, **sans code**, coercée + **validée par le schéma Zod** du module. Expérience **devops**.
- **Override de la config APP** (forme `NF__APP__<CHEMIN>`) — même mécanique, segment réservé `app`
  (jamais un nom de module → 0 collision). Surcharge `domain`, `servers.*`, `log.*`… **validés par le
  Zod app** au resolve. Plus de « comment je surcharge le port / le domaine / le driver de log ? ».

`__` (double underscore) = séparateur de niveau (choix .NET/Docker, sans ambiguïté avec le camelCase) ;
segments **insensibles à la casse**, résolus contre les clés réelles du schéma :

```bash
NF__SECURITY__JWT__ACCESSTTLS=300
NF__HTTP__SERVERS__HTTPS__PORT=8443
NF__SECURITY__CORS__ORIGINS=https://a.com,https://b.com   # CSV → array
NF__APP__DOMAIN=0.0.0.0                                   # config APP (segment réservé `app`)
NF__APP__SERVERS__HTTP__PORT=8080
NF__APP__LOG__DRIVER=file
NF_WEBHOOK_KEY_FILE=/run/secrets/webhook_key             # secret depuis un fichier monté (*_FILE)
```

Une valeur invalide fait **échouer le boot** avec un message nommant la variable (jamais de fallback
silencieux). Un segment mal orthographié (module, app, ou chemin) déclenche un **« vouliez-vous dire
X ? »** façon git, avec les clés disponibles. Non-chevauchement : un champ qui a une variable dédiée au
catalogue de l'APPLICATION (`env.ts`) n'est pas aussi piloté par `NF__*` (la variable nommée fait
foi). Le nom court d'un MODULE suit sa propre règle — voir « Pour commencer » plus haut.

> **Limite assumée pour `NF__APP__*`** : seuls les champs qui ont un **défaut framework** (`domain`,
> `servers.*`, `log.*`) sont surchargeables génériquement. Un champ app **opt-in sans défaut**
> (`domainCheck`, `domainAlias`) doit être déclaré dans `nodefony.config.ts` (ou câblé au catalogue
> `ctx.env`) pour devenir adressable — sinon WARNING + suggestion, jamais de clé fantôme.

## Structure d'un module — une source de vérité (règle d'or)

Chaque module porte sa config dans **un schéma Zod commenté** = la **seule** source de : type
(`z.infer`), validation, **défaut** (`.default()`), doc (`.describe()`) et formulaire Studio
(`z.toJSONSchema`). **Un défaut n'est jamais re-tapé ailleurs.** Forme : `config.ts` (le schéma = QUOI,
lisible) + `defineXConfig.ts` (builder pur = COMMENT, ~15 lignes). **`@nodefony/drizzle` = module de
référence** de cette forme à deux fichiers. Voir
[ADR-0006](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/0006-configuration-unifiee-env-override.md) (D1/D2).

## Héritage quand Nodefony est une dépendance

Un projet qui fait `npm i nodefony` a **les mêmes** `nodefony.config.ts` + `env.ts`, **hérite**
automatiquement des défauts du core et de chaque module (deep-merge au boot), n'écrit que ses
**écarts**, et son devops surcharge par `NF__*`/`*_FILE` en Docker — **sans toucher au code** du
projet ni du framework. C'est le modèle Spring Boot starter / Symfony bundle, transposé en TS.

## 📖 Lexique

| Terme               | Ce que c'est                                                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`defineConfig`**  | La fonction qui compose la configuration de l'application (`defineConfig.ts#defineConfig`). Elle ne lit jamais l'environnement — elle assemble.    |
| **`env.ts`**        | Le **seul** endroit qui lit l'environnement. Tout le reste reçoit des valeurs déjà résolues, ce qui rend la configuration testable sans variables. |
| **`use()`**         | Déclarer un module et le configurer **avec ses types** (`use.ts#use`) : une clé mal orthographiée ne compile pas.                                  |
| **Fusion profonde** | Les défauts du cœur et de chaque module servent de socle ; votre fichier n'écrit que les **écarts**.                                               |
| **Provenance**      | D'où vient chaque valeur retenue : défaut, fichier, variable d'environnement. Elle reste consultable après le boot.                                |
| **`hot` / `boot`**  | Une clé est soit relue à chaud, soit figée au démarrage. Confondre les deux fait croire qu'un réglage « ne prend pas ».                            |

## ⚠️ Pièges

- **Une clé qu'un module n'a pas déclarée fait échouer le démarrage.** Les schémas des modules sont
  stricts (`z.strictObject`) : une faute de frappe lève une `BootConfigurationError` qui la nomme.
  `use()` avance l'erreur à la compilation. Seule la racine de la configuration d'application écarte
  encore une clé inconnue au lieu de la refuser.
- **Ne jamais déréférencer le kernel à l'évaluation d'un fichier de configuration.** Il n'existe pas
  encore au moment de l'import : le module devient non importable et non testable. Utilisez un
  accesseur (`get filename() { … }`), résolu à la lecture.
- **Modifier une clé `boot` sans redémarrer ne change rien**, et rien ne le signale. Vérifiez le
  régime de la clé avant de conclure que la configuration est ignorée.
- **En développement, une modification de configuration demande une reconstruction** — la section
  dédiée ci-dessus dit laquelle. Un réglage qui « ne prend pas » vient souvent de là.
- **Les variables d'environnement du framework se préfixent `NF_`.** Les noms génériques
  appartiennent à d'autres outils, et une collision ne se manifeste jamais par une erreur : juste
  par un comportement inexplicable. Le préfixe dit **à qui appartient la valeur**, pas qui la lit :
  ce que votre application se donne à elle-même le porte aussi. Trois choses en sont dispensées —
  ce que l'on ne possède pas (`NODE_ENV`, `CI`), les alias qu'un hébergeur pose lui-même
  (`DATABASE_URL`, `REDIS_URL`, acceptés en second rang derrière la forme `NF_`), et les
  identifiants **émis par un service tiers** dont l'écosystème fixe déjà le nom
  (`GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_SECRET`) : ceux-là, vous les avez déjà, Nodefony les lit tels
  quels plutôt que de vous les faire recopier.

## 🧪 Tests & couverture

Les chiffres exacts vivent dans la carte de l'aperçu, régénérée depuis vitest — jamais figés ici.

| Type | Où | Ce qui est prouvé |
| --- | --- | --- |
| Unitaires (composition) | `nodefony` `defineConfig.test.ts`, `configUse.test.ts`, `configBoot.test.ts` | l'assemblage, le typage par module, ce qui est figé au démarrage |
| Unitaires (environnement) | `nodefony` `defineEnv.test.ts`, `loadEnv.test.ts`, `envOverride.test.ts`, `envExample.test.ts` | la lecture unique de l'environnement, les surcharges `NF__*`, le fichier d'exemple engendré |
| Unitaires (provenance) | `nodefony` `configProvenance.test.ts`, `infra.test.ts`, `podEnvironment.test.ts` | d'où vient chaque valeur retenue, et ce que le pod ajoute |
| Unitaires (par module) | `@nodefony/framework` `config.test.ts`, `configMutation.test.ts` · `@nodefony/http` `httpConfig.test.ts` · `@nodefony/security` `defineSecurityConfig.test.ts` · `@nodefony/realtime` `defineRealtimeConfig.test.ts` | que chaque module valide bien la sienne |

## 🔗 Pour aller plus loin

- ⬆️ **Retour au hub** : [Guides](../) · [Toute la documentation](../../)
- 🏛️ **Le concept, et non la recette** :
  [architecture — configuration](../../architecture/configuration/)
- 🗄️ **Déclarer son infrastructure plutôt que huit backends** :
  [`persistence.md`](../persistence/)
- 🐳 **Surcharger en conteneur** : [`docker-cloud-native.md`](../docker-cloud-native/)
- 🔄 **Le moment où la configuration est lue** :
  [cycle de boot du Kernel](../../architecture/cycle-boot-kernel/)
- 📖 [Lexique général](../../lexique/) du framework.
