---
title: "Nodefony — vue d'ensemble"
source: docs/architecture/vue-ensemble.md
url: https://nodefony.github.io/nodefony-core/docs/architecture/vue-ensemble/
---


> Nodefony est un framework Node.js **fullstack** en TypeScript dont le parti pris tient en une
> phrase : **HTTP et WebSocket sont co-citoyens**. Même noyau, même conteneur de services, même
> routeur, même firewall, et surtout **le même contexte de contrôleur** — une route web et un canal
> temps réel s'écrivent dans la même classe, avec les mêmes décorateurs. Cette page pose la carte du
> territoire : ce qu'est le framework, ce qu'il n'est pas, ses partis pris et ce qu'ils coûtent.

📍 [Documentation](../../) › **Vue d'ensemble**

## 🧠 Le modèle mental — un moteur, deux portes d'entrée

La plupart des stacks Node ont **deux moteurs** : un pour le web (Express, Fastify) et un pour le
temps réel (Socket.IO). Deux routages, deux sessions, deux façons de vérifier un droit. Nodefony n'en
a qu'**un** — les transports sont deux portes sur le même couloir.

```mermaid
flowchart LR
  CH(["Navigateur · app · script"]) -->|HTTP/1.1 · HTTP/2| SRV["Serveurs<br/>node:http · node:http2 · ws"]
  CH -->|WebSocket| SRV
  SRV --> HK["HttpKernel<br/>pipeline unique"]
  HK --> CTX["Context<br/>(HttpContext | WebsocketContext)"]
  CTX --> FW["Firewall<br/>zones · identité · droits"]
  CTX --> RR["Router → Resolver"]
  FW --> CTRL["TON contrôleur<br/>une classe, deux transports"]
  RR --> CTRL
  K["Kernel<br/>boot · modules · arrêt"] --- SRV
  K --- DI["Container (DI)<br/>+ scope par requête"]
  DI -. scope .- HK
```

Trois pièces suffisent à tout comprendre :

1. le **Kernel** allume l'application et charge les **modules** ;
2. le **Container** distribue les services, avec un sous-annuaire jetable par requête ;
3. le **Context** porte la requête — qu'elle soit HTTP ou WebSocket — jusqu'à ton contrôleur.

## 📖 Lexique

| Sigle / terme | Développé                            | En une ligne                                                                           |
| ------------- | ------------------------------------ | -------------------------------------------------------------------------------------- |
| **Kernel**    | (noyau)                              | Le chef d'orchestre : lit la config, charge les modules, ouvre les serveurs, arrête.   |
| **Module**    | (unité chargeable)                   | Un paquet greffable sur le cycle de vie. Ton application **est** un module.            |
| **Container** | (conteneur de services)              | L'annuaire des services partagés, et le porteur des portées.                           |
| **Scope**     | (portée)                             | Sous-annuaire créé par requête, jeté à la fin — l'isolation entre requêtes.            |
| **Context**   | (contexte de requête)                | L'objet qui porte une requête de bout en bout, HTTP **ou** WebSocket.                  |
| **Resolver**  | (résolveur)                          | Ce qui traduit « route matchée » en « méthode de contrôleur appelée ».                 |
| HTTP/HTTPS    | HyperText Transfer Protocol (Secure) | Le protocole requête→réponse du web ; « S » = chiffré par TLS.                         |
| HTTP/2        | —                                    | Version multiplexée de HTTP (plusieurs échanges sur une seule connexion).              |
| WS/WSS        | WebSocket (Secure)                   | Canal bidirectionnel persistant ; « S » = chiffré par TLS.                             |
| TLS           | Transport Layer Security             | La couche de chiffrement sous HTTPS et WSS.                                            |
| DI            | Dependency Injection                 | Fournir à un objet ses dépendances au lieu qu'il les fabrique lui-même.                |
| ALS           | AsyncLocalStorage                    | Stockage Node.js qui suit une requête à travers tout l'asynchrone (id, trace, user).   |
| BFF           | Backend-For-Frontend                 | Le serveur gère session et jetons pour le front web (cookie opaque, pas de JWT au JS). |
| CSP           | Content-Security-Policy              | En-tête qui limite les sources de scripts (anti-XSS).                                  |
| CSRF          | Cross-Site Request Forgery           | Un site tiers déclenche une requête authentifiée à l'insu de la victime.               |
| CSWSH         | Cross-Site WebSocket Hijacking       | La variante CSRF appliquée à l'ouverture d'un WebSocket.                               |
| CORS          | Cross-Origin Resource Sharing        | Les règles qui autorisent (ou non) un site à lire la réponse d'une autre origine.      |
| RBAC          | Role-Based Access Control            | Des droits accordés selon le rôle porté par l'utilisateur.                             |
| JWT           | JSON Web Token                       | Jeton signé, auto-porté, présenté par un client machine.                               |
| ORM           | Object-Relational Mapping            | La couche qui traduit objets ↔ lignes de base de données.                              |
| HMR           | Hot Module Replacement               | Le rechargement à chaud du frontend, sans perdre l'état de la page.                    |
| RFC           | Request For Comments                 | Les standards officiels d'Internet (IETF).                                             |

## Qu'est-ce que Nodefony — et ce qu'il n'est pas

**Un framework applicatif complet**, au sens où il fournit d'emblée ce qu'une application de
production finit toujours par redemander : un cycle de vie de démarrage discipliné, une injection de
dépendances, un routeur à décorateurs, des sessions, un pare-feu applicatif, une couche ORM, un
builder frontend et une console d'administration.

Ce qu'il **n'est pas**, dit franchement :

- **Ce n'est pas un micro-framework HTTP.** Express et Fastify te donnent un routeur et te laissent
  assembler le reste ; Nodefony arrive avec le reste déjà assemblé. Si ton besoin est « servir trois
  routes JSON », Nodefony est surdimensionné — et c'est une réponse honnête.
- **Ce n'est pas un clone de Symfony ni de NestJS.** Des idées éprouvées sont reprises (conteneur de
  services, événements de cycle de vie, firewall par zones, contrôleurs à décorateurs), mais
  l'architecture, la terminologie et surtout le modèle de transport unifié lui sont propres.
- **Ce n'est pas un runtime alternatif.** Les serveurs s'appuient sur Node.js natif :
  `node:http` et `node:http2` (`ServerHttp.createServer()`, `server-http.ts#ServerHttp.createServer`), `node:https`
  (`ServerHttps.createServer()`, `server-https.ts#ServerHttps.createServer`), et la bibliothèque `ws` pour le WebSocket
  (`Websocket.createServer()`, `server-websocket.ts#Websocket.createServer`). Pas de moteur maison, pas de `Bun.serve()`.
- **Ce n'est pas une bibliothèque temps réel greffée à côté.** Le WebSocket n'est pas un module
  optionnel branché sur le serveur web : il traverse **le même pipeline**, avec la même session et le
  même firewall.

> [!NOTE]
> Nodefony vise deux familles d'applications avec le même socle : les applications web temps réel
> (le web et le canal poussé cohabitent nativement) et les back-ends d'agents IA (jetons machine,
> scopes, idempotence, audit). Le cœur reste **générique** — aucune logique métier n'y vit.

## La vision Nodefony — HTTP et WebSocket, co-citoyens

L'affirmation se vérifie à trois endroits du code, pas dans un slogan.

**1. Une seule classe de base porte la requête.** `HttpContext` et `WebsocketContext` héritent tous
deux de `Context` — l'objet qui transporte session, utilisateur, identifiant de requête et décision
du firewall :

| Transport | Classe                                   | Ancrage                                |
| --------- | ---------------------------------------- | -------------------------------------- |
| Commun    | `class Context extends Service`          | `Context.ts#Context`                   |
| HTTP/2    | `class HttpContext extends Context`      | `HttpContext.ts#switchUrlScheme`       |
| WebSocket | `class WebsocketContext extends Context` | `WebsocketContext.ts#WebsocketContext` |

**2. Les deux traversent le même résolveur.** `HttpContext.handle()` appelle `router.resolve(this)`
puis `resolver.callController()` (`HttpContext.ts#HttpContext._onTimeout`) ; `WebsocketContext.handle()` fait exactement
la même chaîne, en passant en plus les données de la trame (`WebsocketContext.ts#WebsocketContext.handle`). Un seul
`Router` (`router.ts#Router`), un seul `Resolver` (`Resolver.ts#Resolver`).

**3. Une route déclare ses transports, elle ne choisit pas son monde.** Le transport est une
**exigence de route** parmi d'autres : `Route.match()` compare `context.method` — qui vaut
littéralement `"WEBSOCKET"` sur une socket — au `methodsSet` précompilé de la route
(`Route.ts#Route.methodsSet`). Une même action peut donc déclarer `methods: ["GET", "WEBSOCKET"]` et répondre aux
deux portes.

Ce que ça change concrètement : **une** session, **un** modèle de droits, **un** identifiant de
requête tracé, **un** journal — que l'appel arrive en HTTP/1.1, en HTTP/2 ou par une trame WebSocket.
Ce que porte `Context` pour les deux transports : `router` (`Context.ts#Context.router`), `resolver`
(`Context.ts#Context.resolver`), `session` (`Context.ts#Context.session`), `user` (`Context.ts#Context.user`), la décision du firewall
(`Context.ts#Context.user`), le `requestId` (`Context.ts#Context.requestId`) et le nonce CSP paresseux (`Context.ts#Context.requestId`).

## 🚀 Démarrage rapide

**Le besoin.** Tu veux une application qui serve une route JSON **et** un écho temps réel, sans
monter deux serveurs ni dupliquer ta configuration.

`npm create nodefony@beta mon-app` génère le squelette : `nodefony.config.ts` (les écarts aux défauts
du framework), `env.ts` (le catalogue typé des variables d'environnement), `index.ts` (le module de
l'app) et un premier contrôleur. Voici les deux fichiers qui comptent.

### 1. La configuration — `nodefony.config.ts`

```typescript
// nodefony.config.ts — UN fichier : seulement les ÉCARTS aux défauts du framework.
// Le par-environnement passe par `ctx`, jamais par un fichier parallèle.
export default defineConfig(() => ({
  // Un conteneur doit écouter toutes les interfaces : un bind 127.0.0.1
  // n'est jamais atteint par le port mapping Docker/k8s.
  domain: "127.0.0.1",
  // Manifeste ORDONNÉ : l'ordre du tableau EST l'ordre de chargement.
  modules: [
    // Socle serveur : HTTP/1.1, HTTP/2 et WebSocket natifs.
    use("@nodefony/http", {}),
    // Routeur + contrôleurs + décorateurs (@controller, @route).
    "@nodefony/framework",
  ],
}));
```

### 2. Le contrôleur — un fichier, deux transports

```typescript
// nodefony/controllers/HelloController.ts — complet, compile tel quel
import {
  route,
  controller,
  Controller,
  CurrentUser,
} from "@nodefony/framework";
import type { ContextType } from "@nodefony/http";
import type { IUser } from "@nodefony/user";

@controller("/api")
class HelloController extends Controller {
  constructor(context: ContextType) {
    super("hello", context);
  }

  // Route HTTP classique. `@CurrentUser()` injecte l'utilisateur résolu par la
  // zone firewall qui couvre ce chemin — hors zone, il n'est jamais résolu.
  @route("hello-index", { path: "/hello", method: "GET" })
  async index(@CurrentUser() user?: IUser) {
    return this.renderJson({
      hello: "mon-app",
      who: user?.identifier ?? "anonyme",
    });
  }

  // MÊME classe, MÊME décorateur : seul le transport déclaré change.
  // L'action reçoit en plus le contenu de la trame reçue.
  @route("hello-echo", {
    path: "/echo",
    requirements: { methods: ["WEBSOCKET"] },
  })
  async echo(message: string | Buffer | null) {
    if (!message) return this.renderJson({ handshake: true });
    return this.renderJson({ echo: message.toString() });
  }
}

export default HelloController;
```

### 3. Ce qu'on observe

```bash
# Démarrage en développement (rebuild + redémarrage automatiques).
npx nodefony development

# 1) La porte HTTP
curl -s http://127.0.0.1:5151/api/hello
# {"hello":"mon-app","who":"anonyme"}

# 2) La porte WebSocket — MÊME contrôleur, même pipeline, même journal
wscat -c ws://127.0.0.1:5151/api/echo
# > bonjour
# < {"echo":"bonjour"}
```

Aucune passerelle à déclarer, aucun serveur temps réel à monter, aucune configuration en double.
C'est le différenciateur, en trente lignes.

## 🗂️ La carte des modules

Un module Nodefony est une unité **déclarée**, jamais découverte par magie : le manifeste
`modules` de `nodefony.config.ts` est lu par `Kernel.resolveModuleEntries()` (`Kernel.ts#Kernel.resolveModuleEntries`) puis
chargé par `Kernel.loadModulesFromManifest()` (`Kernel.ts#Kernel.loadModulesFromManifest`). L'ordre du tableau **est** l'ordre
de chargement ; la résolution ne fait que **filtrer** (une entrée `policy: "dev"` disparaît hors
développement, une garde `when(config)` fausse écarte l'entrée).

```mermaid
flowchart BT
  core["nodefony (cœur)<br/>Service · Container · Kernel · Syslog"]
  http["@nodefony/http<br/>serveurs · contextes · sessions"]
  fw["@nodefony/framework<br/>Router · Controller · décorateurs"]
  sec["@nodefony/security<br/>firewall · auth · audit"]
  user["@nodefony/user"]
  orm["@nodefony/orm-core"]
  rt["@nodefony/realtime"]
  front["@nodefony/frontend"]
  studio["@nodefony/studio"]
  app["TON application"]

  http --> core
  fw --> http
  user --> orm
  orm --> core
  sec --> user
  sec --> fw
  rt --> fw
  front --> fw
  studio --> front
  app --> fw
  app --> sec
```

> [!IMPORTANT]
> `@nodefony/http` ne peut **jamais** importer `@nodefony/framework` : ce serait une dépendance
> circulaire. Le transport accède au résolveur par le contexte, jamais par un import. C'est la
> contrainte qui garde le socle réutilisable.

### Choisir en cinq secondes

| Famille | Modules | Tu en as besoin quand… |
| --- | --- | --- |
| **Cœur runtime** | [`nodefony`](../../modules/core/) · `http` · `framework` | toujours — c'est le socle |
| **Sécurité** | `security` · `user` | tu as des comptes, des droits, des jetons |
| **Données** | `orm-core` · `drizzle` · `mongoose` · `redis` | tu persistes quoi que ce soit |
| **Temps réel** | `realtime` | tu multiplexes des canaux au-dessus du WebSocket |
| **Frontend** | `frontend` | tu sers une SPA (Vite, HMR, multi-framework) |
| **Administration** | `studio` · `documentation` | tu veux voir l'instance tourner de l'intérieur |

### [`nodefony`](../../modules/core/) — le socle

`Service` (`Service.ts#Service`) est la classe de base de **tout** : Kernel, Module, contrôleurs, services
métier. Elle apporte l'accès au conteneur, l'émission d'événements et le journal structuré. Au-dessus
vivent `Kernel` (`Kernel.ts#Kernel`), `Module` (`Module.ts#Module`) et `Container` (`Container.ts#Container.id`).
Commence par sa page si tu veux comprendre « pourquoi tout hérite de la même chose ».

### [`@nodefony/http`](../../modules/http/) — le transport

Serveurs HTTP/1.1, HTTP/2, HTTPS et WebSocket, contextes de requête, sessions, certificats TLS. C'est
ici que naît le `Context` que ton contrôleur reçoit, et ici que vit le pipeline unique
(`HttpKernel`, `http-kernel.ts#HttpKernel`). À lire quand tu touches au transport, aux sessions ou aux
en-têtes.

### [`@nodefony/framework`](../../modules/framework/) — écrire des routes

`Router` (`router.ts#Router`), `Controller` (`Controller.ts#Controller`), `Resolver` (`Resolver.ts#Resolver`) et les
décorateurs que tu utilises tous les jours : `controller()` (`routerDecorators.ts#controller`), `route()`
(`routerDecorators.ts#route`), `Get` (`routerDecorators.ts#Get`), `IsGranted()`
(`routerDecorators.ts#IsGranted`), `CurrentUser` (`routerDecorators.ts#CurrentUser`). C'est la surface que tu
manipules le plus.

### [`@nodefony/security`](../../modules/security/) — protéger l'application

Pare-feu applicatif par zones, stratégies d'authentification, autorisation par rôles et voters,
CSRF/CORS/en-têtes, audit. La brique la plus fournie du dépôt — et la seule dont la mauvaise
configuration se paie en incident.

## 🏗️ Le moteur en marche

Trois mouvements, résumés ici ; chacun a sa page dédiée, plus détaillée.

### Le boot — une chaîne de phases, jamais un big-bang

Le démarrage est une suite d'**événements ordonnés**, déclarés en masque de bits
(`Events`, `Kernel.ts#Events`) : `onInit` → `onPreStart` → `onStart` → `onPreRegister` → `onRegister` →
`onPreBoot` → `onBoot` → `onReady` → `onServersReady` → `onPostReady`. La chaîne est portée par
`Kernel.start()` (`Kernel.ts#Kernel.start`), `Kernel.boot()` (`Kernel.ts#Kernel.boot`), `Kernel.onReady()`
(`Kernel.ts#Kernel.onReady`) et `Kernel.initServers()` (`Kernel.ts#Kernel.initServers`).

Un module se greffe sur ces phases en définissant `onKernelRegister`, `onKernelBoot` ou
`onKernelReady` : `Module.setEvents()` (`Module.ts#Module.setEvents`) les attache, et n'attache **que** ceux qui
existent — pas de listener orphelin.

> [!TIP]
> Les phases sensibles passent par `Kernel.fireLifecycle()` (`Kernel.ts#Kernel.fireLifecycle`), qui borne chaque hook
> par un délai et par la criticité du module. Un module non critique qui échoue à son boot ne tue pas
> le process (`Kernel.recordBootFailure()`, `Kernel.ts#Kernel.recordBootFailure`) : c'est la résilience « fail-soft ».
> Le détail complet, y compris le verdict de boot et l'arrêt drainé →
> [cycle de boot du Kernel](../cycle-boot-kernel/).

### Une requête — le trajet en gros plan

```mermaid
flowchart LR
  IN["Requête entrante"] --> SC["enterScope('request')<br/>un sous-annuaire jetable"]
  SC --> CX["Création du Context<br/>(HttpContext ou WebsocketContext)"]
  CX --> ALS["Bulle ALS<br/>requestId · trace · contexte"]
  ALS --> DEF["Défenses<br/>CORS · en-têtes"]
  DEF --> RT["Router.resolve<br/>route matchée"]
  RT --> BODY["Corps parsé<br/>puis CSRF"]
```

Puis l'identité, et seulement ensuite votre code :

```mermaid
flowchart LR
  SESS["Session (paresseuse)"] --> FWD{"Zone protégée ?"}
  FWD -->|non| RES["Resolver → action"]
  FWD -->|oui| AUTH["Firewall<br/>identité + droits"]
  AUTH --> RES
  RES --> CTRL["Ton contrôleur"]
  CTRL --> OUT["Réponse HTTP<br/>ou trame WebSocket"]
  OUT --> TD["Teardown<br/>journal · leaveScope · nettoyage"]
```

Les points d'ancrage, dans l'ordre réel (`http-kernel.ts`) : `HttpKernel.onHttpRequest()`
reçoit la requête et pose le socle d'en-têtes ; `HttpKernel.handle()` ouvre le scope
(`enterScope("request")`) ; `HttpKernel.createHttpContext()` construit le contexte
et pose **un unique** `response.on("close")` qui déclenchera le teardown ;
`HttpKernel.handleHttp()` entre dans la bulle `RequestContext.run`, applique CORS
, résout la route puis pose les en-têtes de sécurité ; `HttpKernel.onRequestEnd()`
enchaîne CSRF, session (`HttpKernel.startSession()`) et firewall
; enfin `HttpKernel.teardownHttp()` journalise, quitte le scope et nettoie.

Côté WebSocket, la même partition, avec une règle de plus : **rien ne s'ouvre tant que le pipeline
n'a pas dit oui**. `HttpKernel.onWebsocketUpgrade()` borne le handshake et ouvre le scope
AVANT le `101`, `HttpKernel.createWebsocketContext()` pose un `once("onFinish")`
qui sauvegarde la session et libère le scope, `HttpKernel.onConnect()` vérifie l'origine
(`HttpKernel.checkWebsocketOrigin()` — la garde anti-CSWSH) puis passe la main au firewall et
aux gardes ; seule leur décision favorable accepte la socket. Un refus est un **statut HTTP** au
handshake (`toHandshakeStatus()`, `WebsocketContext.ts#toHandshakeStatus` — un `1008` hérité devient un 403) ;
une fois la socket ouverte, `toWsCloseCode()` (`WebsocketContext.ts#toWsCloseCode`) traduit un statut en code de
fermeture conforme RFC 6455 §7.4 — un 401 devient un `1008`.

Le pas-à-pas exhaustif, avec le détail de chaque garde →
[pipeline de requête](../pipeline-requete/).

### L'injection — la carte et les calques

Le conteneur racine tient les services partagés : c'est la **carte**. À chaque requête HTTP (et à
chaque connexion WebSocket), `Container.enterScope()` (`Container.ts#Container.enterScope`) pose dessus un
**calque** — un scope qui **adopte le prototype** du conteneur racine (`Scope`, `Container.ts#Scope`).
On lit toute la carte à travers : résoudre un service partagé depuis un scope ne coûte aucun saut
logiciel, c'est le moteur JavaScript qui remonte la chaîne de prototypes.

Ce que la requête pose, elle le pose sur son calque seulement (`Scope.set()`, `Container.ts#Container.set`) :
le pipeline y range le `context` et le `controller`, et un service déclaré
`@injectable({ scope: "request" })` y vit le temps de la requête. Deux requêtes concurrentes ne se
voient jamais, et `Container.leaveScope()` (`Container.ts#Container.leaveScope`) jette le calque à la fin — en
appelant le `clean()` des services créés pour elle. Depuis n'importe quel code de la requête,
`RequestContext.getScope()` rend le calque courant, sans recevoir le contexte.

Les décorateurs, l'ordre d'instanciation et les pièges de portée →
[injection & portées](../injection-portees/). Comment la configuration alimente tout ça →
[configuration](../configuration/).

## 🔐 La sécurité en un coup d'œil

Le pare-feu applicatif de `@nodefony/security` raisonne par **zones** : un motif d'URL, une politique.
`Firewall.matchPath()` (`firewall.ts#Firewall.matchPath`) rattache la requête à la zone dont le motif est le plus
spécifique ; `Firewall.isSecure()` (`firewall.ts#Firewall.isSecure`) répond « protégée ou non » sur le chemin chaud ;
`Firewall.handleSecurity()` (`firewall.ts#Firewall.handleSecurity`) ne travaille que sur zone protégée.

```mermaid
flowchart TD
  R["Requête (HTTP ou WS)"] --> Z{"Zone protégée ?"}
  Z -->|non| PASS["passe — l'identité n'est même pas résolue"]
  Z -->|oui| A["Authenticators de la zone, dans l'ordre"]
  A -->|trop de tentatives| T429["429 + Retry-After"]
  A -->|credential invalide| E401["401 + challenge"]
  A -->|aucune preuve| Z401["401 — Zero Trust"]
  A -->|succès| OK["user + jeton dans l'ALS → contrôleur"]
```

Deux points à retenir dès maintenant :

- **La politique par défaut est Zero Trust** : sur une zone protégée, pas de preuve valide = 401. Une
  route « publique » qui veut néanmoins connaître l'utilisateur doit lister `anonymous`
  explicitement — c'est l'authenticator enregistré à `authenticatorRegistry.ts`.
- **Le WebSocket est couvert par le même firewall** : la zone est résolue pareil, et l'autorisation
  descend jusqu'aux canaux.

La carte complète (zones, authenticators, rôles, voters, audit) →
[hub sécurité](../../modules/security/) puis
[firewall](../../modules/security/firewall/).

## Je viens d'ailleurs — Express, NestJS, Symfony

Trois profils, trois façons d'atterrir. Ce qui se ressemble, ce qui change, ce qu'il faut désapprendre.

### Je viens d'Express

**Ce qui se ressemble.** Le routage reste « un chemin, une méthode, une fonction ». La stratégie de
résolution t'est même familière : Nodefony est **router-first** — si aucune route ne matche, on
retombe sur le service de fichiers statiques (`http-kernel.ts#HttpKernel.dispatchHttpRequest` et suivantes), exactement comme
un `express.static()` placé en fin de chaîne.

**Ce qui change.** Il n'y a pas de `app.use(middleware)` empilable à volonté. Le pipeline est **fixe
et ordonné** (en-têtes et CORS → routage → corps → CSRF → session → firewall → contrôleur), et tu t'y greffes par des points
d'accroche nommés : `onCreateContext`, `beforeResolve`, `afterAuth`, `onAuthFailure`. Ces seams sont
gardés par `listenerCount` — sans écouteur, ils ne créent **aucune** microtask.

**Ce qu'il faut désapprendre.** Le réflexe « je poserai un middleware pour ça ». En Nodefony, une
préoccupation transverse est soit un **service** injecté, soit un **décorateur** sur l'action
(`@IsGranted`, `@Idempotent`, `@Csp`), soit un **module** qui s'abonne à une phase. L'ordre n'est
alors plus une propriété fragile de ton fichier d'entrée.

### Je viens de NestJS

**Ce qui se ressemble.** Beaucoup : les contrôleurs à décorateurs (`@controller`, `@Get`, `@Param`,
`@Body`, `@Query`), l'injection de dépendances, les gardes d'autorisation, l'idée de modules. Une
garde `@IsGranted` s'applique **avant** l'instanciation du contrôleur, au niveau du résolveur — même
contrat qu'un `CanActivate` : `Resolver.executeActionGuarded()` (`Resolver.ts#Resolver.executeActionGuarded`) enveloppe
l'action, et `Resolver._enforceSecurity()` (`Resolver.ts#Resolver._enforceSecurity`) tranche, fail-closed en 403.

**Ce qui change.** Deux choses. D'abord, **pas de gateway WebSocket séparée** : là où Nest te
demande un `@WebSocketGateway()` distinct de tes contrôleurs, Nodefony te fait déclarer le transport
sur la route (`requirements: { methods: ["WEBSOCKET"] }`) — même classe, même session, même firewall.
Ensuite, le conteneur résout **par nom** dans un annuaire, pas par un graphe de providers compilé par
module ; la portée par requête est un scope réel, ouvert et fermé par le pipeline.

**Ce qu'il faut désapprendre.** L'idée qu'un « module » doit déclarer `imports`, `providers` et
`exports` pour que l'injection fonctionne. Ici un module déclare ses services avec `@services([...])`
et le Kernel les instancie dans un ordre calculé par tri topologique de leurs dépendances.

### Je viens de Symfony

**Ce qui se ressemble.** Le vocabulaire, et ce n'est pas un hasard : kernel avec des événements de
cycle de vie, conteneur de services, firewall par zones, rôles hiérarchiques, voters pour
l'autorisation. Si `security.firewalls` et `role_hierarchy` te parlent, tu liras la config de
`@nodefony/security` sans lexique.

**Ce qui change.** La configuration est du **TypeScript typé et validé** — un seul
`nodefony.config.ts` avec des fonctions `(ctx) => …` pour le par-environnement, plus un `env.ts` qui
est le seul lecteur de `process.env`. Il n'y a ni YAML, ni fichier par environnement, ni étape de
compilation du conteneur : les services sont instanciés au boot, dans l'ordre de leurs dépendances.

**Ce qu'il faut désapprendre.** Chercher un équivalent ligne à ligne. Nodefony **n'est pas** un
portage de Symfony en TypeScript : les invariants de sécurité sont proches parce qu'ils sont bons,
mais le modèle de transport unifié et l'orientation temps réel n'ont pas d'équivalent côté PHP. Et
la **portée de requête** ne vient pas de Symfony : ses scopes de conteneur ont été
[dépréciés en 2.8 et retirés en 3.0](https://symfony.com/doc/2.8/service_container/scopes.html),
parce qu'un processus PHP classique sert une requête à la fois. Un processus Node en sert des
centaines entrelacées : chaque requête y reçoit son propre conteneur, par chaîne de prototypes
([ADR-0011](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/0011-scope-de-requete-par-copie-prototypale.md)).

## Les partis pris — et ce qu'ils coûtent

Un choix d'architecture qui ne coûte rien n'est pas un choix. Voici les nôtres, avec leur facture.

| Parti pris                                  | Ce qu'il apporte                                                     | Ce qu'il coûte                                                                               |
| ------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| **Pipeline fixe** plutôt que middlewares    | Ordre garanti, défenses jamais contournées par erreur                | Moins de liberté d'insertion : il faut passer par un seam ou un décorateur                   |
| **HTTP et WS dans le même contexte**        | Une session, un firewall, un journal pour les deux                   | Le socle transport est plus gros qu'un simple serveur HTTP                                   |
| **Node.js natif** (`node:http`, `ws`)       | Zéro couche d'abstraction exotique, conformité RFC directe           | Pas de gains « magiques » d'un runtime alternatif                                            |
| **Zero Trust par défaut** sur zone protégée | Aucune route protégée n'est ouverte par oubli                        | Une route publique dans une zone doit lister `anonymous` — sinon 401 surprenant              |
| **Modules déclarés** (manifeste ordonné)    | Chargement prévisible, filtrable par environnement, lisible en revue | Rien n'est découvert tout seul : un module oublié dans le manifeste n'existe pas             |
| **TypeScript strict, ESM uniquement**       | Types réels de bout en bout, tree-shaking                            | Pas de `require()`, pas de dépendance CommonJS non convertie                                 |
| **1 process = 1 conteneur** par défaut      | Scaling délégué à l'orchestrateur, logs vers la sortie standard      | Sans orchestrateur, `nodefony cluster -w N` relance N workers ; sinon k8s, systemd ou Docker |
| **Allocation paresseuse partout**           | Coût par requête très bas sur les chemins non utilisés               | Le code interne est plus verbeux (gardes `listenerCount`, initialisations différées)         |

## 📜 Normes appliquées

| Domaine                      | Norme                          | Ancrage code                                                          |
| ---------------------------- | ------------------------------ | --------------------------------------------------------------------- |
| Sémantique HTTP, 405         | RFC 9110                       | `Route.match()` (`Route.ts#Route.match`)                              |
| Challenge d'authentification | RFC 9110 §11                   | `Firewall.handleSecurity()` (`firewall.ts#Firewall.handleSecurity`)   |
| Fermeture WebSocket          | RFC 6455 §7.4                  | `toWsCloseCode()` (`WebsocketContext.ts#toWsCloseCode`)               |
| Partage cross-origin         | Fetch Standard (WHATWG)        | `Firewall.handleCors()` (`http-kernel.ts#HttpKernel.runHttpPipeline`) |
| Anti-CSRF                    | Fetch Metadata + double-submit | `Firewall.enforceCsrf()` (`http-kernel.ts#HttpKernel.armAndGuard`)    |
| Anti-CSWSH (origine WS)      | OWASP WSTG-CLNT-10             | `HttpKernel.checkWebsocketOrigin()`                                   |
| Journal structuré            | RFC 5424                       | `Pdu` (`Pdu.ts#Pdu`) · `Service.log()` (`Service.ts#Service.log`)     |
| Propagation de trace         | W3C Trace Context              | `HttpKernel.handleHttp()` (`http-kernel.ts#HttpKernel.handleHttp`)    |

## ⚡ Performance & mémoire

Nodefony est un framework **runtime** : chaque allocation par requête se multiplie par le débit. La
règle interne est donc l'allocation paresseuse, et elle se lit dans le code.

- **Rien n'est alloué « au cas où ».** Les buckets de scopes du conteneur restent `null` tant
  qu'aucun scope n'est ouvert (`Container.scopes`, `Container.ts#Container.scopes`) ; le tampon de requêtes ORM du
  profileur n'existe qu'en développement (`profilerQueries`, `http-kernel.ts#HttpKernel.serveHttpContext`) ; le nonce CSP
  n'est calculé que si une directive en a besoin (`Context.cspNonce`, `Context.ts#Context.#cspNonce`).
- **Zéro microtask pour un seam inutilisé.** Les points d'accroche optionnels sont gardés par
  `listenerCount` avant tout `await` — sans module de sécurité, ils ne planifient rien.
- **Un seul écouteur de fin de requête.** `createHttpContext()` pose un unique
  `response.on("close")` (`http-kernel.ts#HttpKernel.teardownHttp`) qui déclenche le teardown : pas de paire
  `finish`/`close` à démonter à la main.

Ces choix sont **mesurés**, pas postulés. La suite `memory.test.ts` mesure les octets retenus par
itération (pente sur paliers, GC forcé) sur les requêtes, les crashs, les uploads et les connexions
WebSocket, plus deux comptes exacts : scopes restés ouverts, contextes jamais réclamés. Ses seuils
vivent dans la table `THRESHOLDS` de `tests/helpers/retention.ts`, partagée avec les bancs de charge. Un seuil qui saute est un blocage, pas un avertissement.

## 📡 Observabilité — Studio

Studio est la console d'administration du framework, servie sur `/nodefony`
(`StudioController.ts#StudioController`). Elle montre l'instance **telle qu'elle tourne**, pas telle que les
fichiers la décrivent : configuration résolue, modules chargés, routes déclarées, sessions actives,
journal, firewall, temps réel — et cette documentation elle-même.

Les données viennent d'un plan de données conventionnel : tout module exposant une API
d'administration la publie sous `/nodefony/<module>/api/*`. C'est aussi ce qui permet à Studio
d'afficher les pages de doc d'un module à côté de son code.

En incident, le réflexe utile est toujours le même : **comparer ce que Studio affiche à ce que tu
crois avoir déployé**. Neuf « pourtant j'ai bien configuré ça » sur dix se règlent là.

## ⚠️ Pièges (symptôme → cause → correction)

| Symptôme                                     | Cause                                                                    | Correction                                                            |
| -------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| Un module n'est pas chargé                   | Absent du manifeste, ou `policy: "dev"` hors dev, ou garde `when` fausse | Vérifier l'entrée dans `modules` de `nodefony.config.ts`              |
| `401` sur une route censée être publique     | Zone protégée + aucune preuve : c'est le Zero Trust                      | Lister `anonymous` dans la zone, ou revoir le motif de zone           |
| Une requête WebSocket n'atteint pas l'action | La route ne déclare pas le transport                                     | Ajouter `requirements: { methods: ["WEBSOCKET"] }`                    |
| `405` sur une action pourtant existante      | La méthode demandée n'est pas dans le `methodsSet` de la route           | Déclarer toutes les méthodes voulues (`["GET", "WEBSOCKET"]`)         |
| L'état d'une requête « fuit » sur une autre  | Service posé sur le conteneur racine au lieu du scope de requête         | Poser le service sur le scope, jamais sur le conteneur partagé        |
| `does not provide an export named 'default'` | `import nodefony from "nodefony"` — il n'y a pas d'export par défaut     | `import { Nodefony } from "nodefony"`                                 |
| `does not provide an export named 'kernel'`  | L'ancien singleton exporté n'existe plus                                 | `Nodefony.getKernel()`                                                |
| Le boot se fige au démarrage                 | Un service dont l'initialisation pend (base ou cache injoignable)        | Vérifier l'infrastructure ; cf [cycle de boot](../cycle-boot-kernel/)  |
| Une route ajoutée renvoie 404 au démarrage   | Artefact de build périmé après un changement d'export public             | Reconstruire proprement ; cf [build & bundling](https://github.com/nodefony/nodefony-core/blob/main/docs/architecture/build-bundling.md)    |
| `Cannot read properties of null` à l'import  | Le kernel est déréférencé au chargement d'un fichier de config           | Passer par un getter paresseux ; cf [configuration](../configuration/) |

## 🧪 Tests & couverture

Cette page est **transverse** : elle ne possède aucune suite propre, elle décrit un moteur couvert
par les suites de tous les modules. Les compteurs exacts vivent dans la carte de l'aperçu, régénérée
depuis vitest — jamais figés dans ce fichier.

Ce qui couvre réellement les affirmations de cette page :

- **unitaires** — le cœur (`src/nodefony/src/tests/`) pour `Service`, `Container`, scopes, Kernel et
  syslog ; `@nodefony/framework` pour le routeur, le résolveur et les décorateurs.
- **intégration (serveur réel)** — `@nodefony/http` pour le pipeline complet, HTTP comme WebSocket,
  ainsi que les défenses (CORS, CSRF, en-têtes) ; `src/modules/test` fournit les contrôleurs
  d'intégration exercés par ces bancs.
- **charge et mémoire** — `tests/load/**` et `memory.test.ts` de `@nodefony/http` : plafonds de tas,
  connexions WebSocket soutenues, latence. Suite **séparée** de la non-régression, à lancer avant
  tout changement touchant le pipeline. Outils : skills `nodefony-load-test` et
  `nodefony-check-memory-health`.
- **attaque** — les bancs `*.attack.test.ts` de `@nodefony/security` exercent le firewall et les
  défenses en conditions hostiles. Outil : skill `nodefony-security-review`.

Ce qui **n'existe pas ici** : aucun banc de contrat multi-backend n'est rattaché à cette page (elle
ne décrit aucun contrat à implémentations multiples — ceux-ci vivent dans les modules qui les
possèdent : stores de session, d'idempotence, adaptateurs ORM).

Couverture : `npm run coverage` dans chaque module concerné.

## 🔗 Pour aller plus loin

- ⬆️ **Retour au hub** : [Toute la documentation](../../)
- 🧭 **La suite du parcours « Je découvre Nodefony »** :
  [Cycle de boot du Kernel](../cycle-boot-kernel/) › [Injection & portées](../injection-portees/) ›
  [Le cœur](../../modules/core/)

- Le trajet exact d'une requête, pas à pas → [pipeline de requête](../pipeline-requete/)
- `nodefony.config.ts`, `env.ts`, `use()` et la validation au boot → [configuration](../configuration/)
- Comment le dépôt se construit et se distribue → [build & bundling](https://github.com/nodefony/nodefony-core/blob/main/docs/architecture/build-bundling.md)
- Écrire des routes et des contrôleurs → [`@nodefony/framework`](../../modules/framework/)
- Le transport, les contextes et les sessions → [`@nodefony/http`](../../modules/http/)
- Protéger l'application → [`@nodefony/security`](../../modules/security/)
- Choisir et brancher sa base → [persistance](../../guides/persistence/) ·
  [`@nodefony/orm-core`](../../modules/orm-core/)
- Exploiter en production → [Docker & cloud-native](../../guides/docker-cloud-native/)
- Pourquoi tel choix a été fait → les [décisions d'architecture](https://github.com/nodefony/nodefony-core/blob/main/docs/adr/README.md)
