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

Nodefony — le framework Node.js fullstack

Temps réel natif, développement agentic-ready, sur un socle TypeScript isomorphe. Dix minutes pour comprendre ce que le framework couvre, ce qu'il coûte et ce qui lui manque.

Vous êtes…

Directeur techniqueMon équipe pourra-t-elle reprendre le code seule ?

Libre, une seule personne, bénévole : seule la dernière majeure reçoit des correctifs.

Le parcours complet →
ExploitationComment ça se déploie en conteneur et s'orchestre ?
Développeur frontComment l'interface se bâtit et se recharge à chaud ?
Développeur backComment je persiste mes données, sur quelle base ?
ArchitecteQuels choix ont été faits, et pourquoi ?

Ou directement : la documentation · les mesures · le dépôt, sous licence Apache 2.0.

L'essentiel en trente secondes

Un framework Node.js fullstack, en TypeScript strict, bâti directement sur les modules natifs de la plateforme. Pensé pour le temps réel : le serveur HTTP et le serveur WebSocket partagent le même contexte de contrôleur.

Version
10.0.0
monodépôt, paquets verrouillés
Node
^24.16.0 || ≥ 26.0.0
ESM uniquement
Domaines
15
cœur + paquets publiables
Symboles
5 253
graphe relationnel indexé

Trois propriétés le définissent, et chacune se vérifie dans le code plutôt qu'elle ne se raconte : le temps réel de première classe, l'isomorphisme serveur/navigateur, et une application qui sait se décrire à un outil comme à une personne.

Ce qu'est Nodefony

Nodefony

Nodefony

Le framework Node.js fullstack : temps réel natif, développement agentic-ready, sur un socle TypeScript isomorphe.

Une action de contrôleur. Deux transports. La même session, la même sécurité, le même code.

Licence Apache 2.0 Node 24.16+ ou 26+ TypeScript strict ESM

📖 Documentation · Par où commencer · Application de démonstration


A note on language. Nodefony is written by a French-speaking author. The code — identifiers, public API, types — is in English, because it travels: npm, editor autocompletion, search. The prose — documentation, agent skills, messages — is in French, the language in which the author explains the why most precisely. Coding agents read it without difficulty; human readers may not, and translating the framework’s texts is planned (#378).

Et NestJS ? Nodefony en partage le modèle — injection, décorateurs, modules — et s’en inspire volontiers. Ses priorités portent ailleurs : une même route sert HTTP et WebSocket, avec la même session et le même pare-feu ; et l’application est pensée pour être lue par un agent de code — générateurs pilotables, catalogue, AGENTS.md dérivé du projet. Deux frameworks, deux accents : le bon choix dépend de ce que ton projet met en avant.

État — version 10.0.0 · préversion 10.0.0-beta.7 en ligne, sous le canal beta uniquement : npm install nodefony sert toujours la 7.0.2 (JavaScript), et le fera jusqu’à la sortie stable · branche par défaut main (dernière publication), développement sur dev · ~7 700 tests quand toute l’infrastructure répond (npm run test:all).

⚠️ Ce dépôt est l’atelier du framework, pas ce qu’une application installe — agent : AGENTS.md. Pour juger le produit, lire une application générée (github.com/nodefony/nodefony, sortie brute de create app) ou en générer une : npm create nodefony@beta <app>.


Nodefony en bref

Nodefony est un framework serveur fullstack pour Node.js, écrit en TypeScript strict et bâti directement sur les modules natifs de la plateforme — node:http, node:http2, WebSocket. Il apporte un noyau à injection de dépendances qui donne à chaque requête son propre conteneur, un système de modules, un pare-feu applicatif, une persistance portable, une console d’administration et la construction des frontends.

Né du modèle Symfony, devenu un framework de l’écosystème Node.js. Ses premières versions transposaient Symfony en JavaScript : bundles, conteneur, configuration. La version 10 s’en est nettement écartée pour rejoindre sa famille, l’écosystème Node.js : serveurs natifs de la plateforme, ESM, TypeScript et décorateurs, et pour tout ce que l’écosystème fait déjà bien — Vite pour les frontends, Drizzle pour la persistance, Zod pour la validation — ses outils plutôt que des briques maison.

Sa particularité tient en une propriété : le WebSocket n’y est pas un ajout. C’est un transport de première classe, servi par le même pipeline, la même table de routes et la même sécurité que le HTTP. Une application temps réel s’y écrit comme une application web ordinaire.

Conçu pour le cloud. Un processus par conteneur : l’orchestrateur le réplique et le relance, le framework n’embarque aucun gestionnaire de processus. Sondes de vie et de disponibilité, arrêt propre sur SIGTERM — l’instance quitte la répartition, termine ses requêtes en cours et ferme ses WebSockets proprement —, configuration par variables d’environnement, journaux sur la sortie standard, traçage OpenTelemetry par le canal de diagnostic de Node. L’application générée porte son image et sa tâche de migration ; les manifestes Kubernetes sont documentés, pas encore générés — Docker et cloud · Kubernetes.

npm create nodefony@beta mon-app

Chaque requête a son propre conteneur

Un processus Node sert des centaines de requêtes entrelacées : pendant que l’une attend sa base de données, une autre s’exécute. Nodefony donne donc à chaque requête son propre conteneur de services, posé comme un calque sur celui de l’application.

  • Le mécanisme. Le conteneur du kernel est la carte ; à l’entrée d’une requête, le pipeline pose un calque (enterScope) — un objet dont le prototype est la carte. À la fin, leaveScope le jette.
  • Ce qu’on y fait. On lit toute la carte à travers le calque (les services de l’application s’héritent) ; on n’écrit que sur son calque. Deux requêtes concurrentes ont deux calques et ne se voient jamais ; rien de ce qui est posé sur un calque ne survit à la requête.
  • Le fil qui relie tout : AsyncLocalStorage. Chaque requête s’exécute dans une bulle ALS de Node (RequestContext) : son identifiant, l’utilisateur, la trace W3C et le calque suivent chaque await, jusque dans un service ou un adaptateur ORM, sans qu’on les passe d’appel en appel.
  • Comment on l’atteint. Depuis n’importe quel code de la requête, sans passer le contexte : RequestContext.getScope() rend le calque courant (ou undefined), requireScope() lève en nommant la cause. La bulle ALS de la requête dit lequel est le tien. Un hook onAfterResponse le voit encore ouvert ; une promesse non attendue qui continue après la réponse, non.
  • HTTP et WebSocket. Une requête HTTP = un calque. Une connexion WebSocket = un calque, partagé par tous ses messages, concurrents compris.
  • Des services par requête. @injectable({ scope: "request" }) donne un exemplaire par requête, créé seulement si la requête le demande, et nettoyé (clean(), dans l’ordre inverse des créations) à sa fermeture. Un singleton qui en dépendrait est refusé dès le démarrage, plutôt que de devenir lui-même, sans qu’on l’ait écrit, un service par requête.
  • Le coût, avec son décor. Poser puis jeter un calque ≈ 1,43 µs (sonde dans le serveur, un processus en production, paires alternées) ; une requête qui ne résout aucun service par requête ne paie rien de plus.
import { Service, injectable, RequestContext } from "nodefony";
import type { Scope } from "nodefony";

@injectable({ name: "requestStamp", scope: "request" })
export class RequestStamp extends Service {
  readonly requestId: string;
  constructor(scope: Scope) {
    super("requestStamp", scope, false); // un exemplaire pour CETTE requête
    this.requestId = RequestContext.getRequestId() ?? "?";
  }
}

Le détail — les trois durées de vie, l’accès par RequestContext, les pièges : Injection & portées.

Les frontends ne sont pas laissés dehors. Nodefony pilote Vite : en développement il démarre les serveurs de développement — React, Vue, Angular, Svelte —, les surveille, relaie leur rechargement à chaud et sert leurs pages ; en production il construit les bundles et les distribue. Une application peut en porter plusieurs, chacun monté sur sa route.

Le socle, lui, est isomorphe : le même paquet s’importe côté serveur et côté navigateur. Le client temps réel, les règles d’autorisation, les types d’une ressource sont écrits une fois et s’exécutent là où ils servent — une règle corrigée l’est des deux côtés.

Le temps réel, de bout en bout

Nodefony est un framework temps réel. Une application vivante pose toujours les mêmes questions : pousser au navigateur, dans un sens ou dans les deux ; ouvrir des salons et savoir qui s’y trouve ; décider qui entre ; faire passer des octets ; tenir à plusieurs instances ; voir ce qui se passe quand ça coince. Chacune a sa réponse dans le framework, écrite contre la norme qui la définit, et éprouvée par des tests.

Ce qu’il te faut Ce que Nodefony fournit
Échanger dans les deux sens La socket Nodefony : des canaux à volonté sur une seule connexion — le serveur diffuse, le client appelle et attend la réponse, ou pousse sans attendre. JSON-RPC 2.0 sur WebSocket (RFC 6455), dans le pipeline HTTP
Pousser dans un seul sens Les flux SSE (text/event-stream) : une action répond par this.renderSse(), en HTTP/1.1 comme en HTTP/2. Le client isomorphe se reconnecte avec Last-Event-ID, et sait envoyer un POST et des en-têtes
Des salons et leurs membres Canaux à membres : lister qui est là, abonner d’office, éjecter un compte, fermer le salon, sur toutes les instances. Un message privé atteint une personne par publishToUser
Décider qui entre Une règle d’admission par canal (allow, fonction ou service injecté), sous le même pare-feu que le HTTP. Origine vérifiée à la poignée de main ; un jeton expiré ferme la socket avec un code dédié
Faire passer des octets CBOR (RFC 8949), négocié connexion par connexion — détaillé ci-dessous
Tenir à plusieurs instances Un bus de diffusion entre workers ou par Redis, messages scellés (HMAC-SHA256). Un seul producteur par canal et par processus, quel que soit le nombre d’abonnés
Tenir la charge Contre-pression en sortie, débit borné en entrée, cadence de diffusion qui s’adapte à chaque client
Des types de bout en bout Un contrat écrit une fois type le contrôleur, la socket et les hooks React, Vue, Svelte et Angular. Les routes qu’il annonce sont vérifiées au démarrage
Voir et tester Une sonde, la console d’administration, les onglets Socket et SSE de la barre de débogage, un traceur de trames ; une identité d’essai pour tester un canal protégé
gRPC — à venir en 10.0.0-beta.7 Des services gRPC servis par le même pipeline HTTP/2, déclarés depuis un module, avec la réflexion et la santé standard

Chaque besoin a son exemple complet et testé : jeu (déplacements, recherche de partie), salon privé sur invitation, sonde industrielle, tableau de bord partagé, notification privée, tâche d’agent.

Le binaire, sans conversion

JSON reste le format par défaut. Dès que des octets circulent — image, son, mesures de capteur, état d’une partie —, le serveur accepte CBOR, un JSON binaire. Nodefony embarque son propre codec : isomorphe, sans dépendance, environ 5,5 Ko compressés, et téléchargé seulement par la page qui l’importe. Il rend exactement ce que JSON rendrait, à une exception près : un Uint8Array voyage en octets et revient en Uint8Array. Un message ordinaire pèse 30 à 45 % de moins, et 64 Ko d’octets en occupent 65,6 au lieu des 87,4 du base64.

Le codec se négocie connexion par connexion. Le client qui l’offre parle CBOR ; celui qui ne l’offre pas reste en texte et reçoit les mêmes données. Aucun client ne casse, et un canal suivi par les deux sortes de connexions paie un encodage par codec, jamais un par abonné.

// nodefony.config.ts — le serveur accepte CBOR
use("@nodefony/realtime", { codec: "cbor" });
// navigateur — le client l'offre à l'ouverture
import { NodefonySocket } from "nodefony/client";
import { CBOR_FRAME_CODEC } from "nodefony/cbor";

const socket = NodefonySocket.shared({
  url: "/chat/realtime",
  codec: CBOR_FRAME_CODEC,
});

→ La socket Nodefony · Les cas d’usage · Les flux SSE · Le codec CBOR

L’IA : ton application, ouverte aux agents

Un agent d’IA atteint une application par une porte standard : le Model Context Protocol (MCP). Il demande quels outils elle offre, reçoit pour chacun un nom, une description et un schéma d’entrée, puis les appelle. @nodefony/ai donne cette porte à ton application et tient le protocole à ta place — versions, erreurs, autorisation, progression, annulation. Tu écris une fonction ; elle devient un outil, que Claude, ChatGPT, Copilot ou l’agent d’un client savent appeler.

Ce qu’il te faut Ce que Nodefony fournit
Ouvrir une porte MCP npx nodefony create controller mcp --kind mcp génère la porte, deux outils d’exemple et leur test. MCP révision 2026-07-28, clients plus anciens acceptés
Suivre un outil long Sur la même requête HTTP, la réponse passe en flux SSE : l’agent reçoit l’avancement (notifications/progress), puis le résultat — et peut annuler en route, l’outil s’arrête proprement
Servir un agent sans serveur Le transport stdio : npx nodefony ai:stdio, pour une intégration continue ou un agent qui lance lui-même son serveur. Les mêmes outils, la même fonction
Ouvrir en production --expose oauth : jeton exigé, serveur d’autorisation annoncé (RFC 9728), un scope par outil — un appelant ne voit que ce qu’il peut appeler. --expose mixte : des outils publics, les autres réservés
Agir au nom de quelqu’un actsForUser : l’outil s’exécute avec le compte de l’appelant, sous les mêmes règles qu’une route protégée
Dire ce que fait l’outil Chaque outil annonce s’il lit ou s’il modifie, pour que l’agent sache quand demander confirmation ; le test généré refuse un outil qui ne le dit pas
Des outils dans chaque module Un module déclare les siens par getMcpTools() ; une porte les ramasse tous, sans registre à tenir
Développer avec un agent de code @nodefony/devkit ouvre en développement la porte /nodefony/mcp : l’agent interroge l’application qui tourne — carte, santé, routes, trafic, profil d’une route — au lieu de deviner

@nodefony/ai ne monte aucune route tant que tu n’ouvres pas de porte ; paquet expérimental, il suit la spécification MCP et peut évoluer en version mineure, toujours annoncé au journal des versions.

→ Le module IA · La boîte à outils MCP

Démarrage

Une préversion 10.0.0-beta.7 est en ligne sous le canal beta. Une bêta n’est pas une version stable : une rupture reste possible d’une bêta à la suivante — chacune est alors annoncée en tête du CHANGELOG — et elle n’est pas destinée à la production. Elle s’installe en nommant ce canal :

npm create nodefony@beta mon-app
cd mon-app
npm run dev

Le canal se nomme tant que la 10 n’est pas stable : c’est lui qui garantit de recevoir la préversion, et non la 7.0.2 que npm install nodefony sert encore — la version JavaScript d’avant, sans rapport avec ce que cette page décrit.

L’application répond sur http://127.0.0.1:5151 — ou le premier port libre, annoncé au démarrage — et la console d’administration sur /nodefony.

À quoi ressemble ce que la commande produit : github.com/nodefony/nodefony. Ce dépôt n’est pas une démonstration écrite à la main pour la vitrine — c’est la sortie littérale de nodefony create app --preset complete --frontend react, poussée telle quelle et régénérée à chaque publication. Base de données et migration initiale, sécurité et comptes, temps réel, console d’administration, front React, infra docker de développement, chaîne d’intégration continue : tout ce que tu y lis a été engendré, rien n’y a été retouché. C’est le moyen le plus court de juger le code qu’on aura à maintenir, avant d’installer quoi que ce soit.

Générer du code — une application, un module, un contrôleur, une entité et toute sa chaîne :

npx nodefony create module blog --frontend react
npx nodefony create entity Article title:string body:text views:int
npx nodefony create controller admin --kind rest --role ROLE_ADMIN

Chaque générateur montre le plan et le diff avant d’écrire quoi que ce soit. Un refus ne laisse rien derrière lui. Il publie aussi son catalogue en JSON et accepte ses réponses par fichier : la même porte sert la personne au terminal, la console d’administration et un agent.

L’application générée, de la machine à la production

Une application sortie de npm create nodefony naît avec son infra de développement, ses tests, ses contrôles de qualité, sa marche à suivre quand ça casse et sa mise en production. Les chapitres ci-dessous sont extraits tels quels de son README — celui de l’application de démonstration, rendue avec --preset complete --frontend react : ce qui se lit ici est exactement ce qu’elle porte.

Infra de développement (docker)

L’app démarre sans docker : la base est une sqlite locale (var/databases/), retenue à la création. Le compose.yaml fournit le cran au-dessus :

npm run infra:up                          # Redis (sessions partagées, realtime multi-process)
docker compose --profile loki up -d       # + Loki + Grafana (logs centralisés)
npm run infra:down                        # arrêt (les volumes survivent)

Éprouver la topologie de PRODUCTION — l’app derrière son frontal

En production ton application vit derrière un proxy, et c’est lui qui décide de l’adresse cliente retenue, du protocole annoncé et du sort d’une WebSocket silencieuse. Le profil edge monte cette topologie en local — l’application en image, sans aucun port publié, et un frontal nginx devant :

npx nodefony http:certificates            # une fois — le certificat de développement
docker compose --profile edge up -d --build
curl -k https://localhost:8443/livez

La configuration du frontal n’est pas écrite : elle est dérivée de ton application à la construction de l’image (nodefony proxy:generate nginx), avec tes hôtes de confiance, tes fichiers statiques, ta taille de corps acceptée et le battement de tes WebSockets. Tu changes l’application, tu reconstruis : la configuration suit.

Rejouer ta suite de bout en bout à travers le proxy, sans la modifier :

NF_E2E_BASE_URL=https://localhost:8443 \
NODE_EXTRA_CA_CERTS=nodefony/config/certificates/ca/nodefony-root-ca.crt.pem \
  npm run test:e2e

Le certificat est monté, jamais gravé dans l’image : une clé privée dans une image reste lisible par qui la télécharge, même effacée par une couche suivante. En production, c’est ton hébergeur ou ton ingress qui le fournit.

Câblage côté app — une variable, tout le reste se dérive (store: "auto") :

NF_REDIS_URL="redis://:[email protected]:6379"   # dans .env

Pour passer sur une vraie base SQL : déclare NF_DATABASE_URL. Le dialecte est déduit du scheme de l’URL (postgres://, mysql://, sqlite:) — changer de base ne change rien d’autre dans l’app. Le service docker correspondant n’est pas dans ce compose.yaml (une app y retient un seul dialecte) : ajoute-le, ou recrée une app avec nodefony create app <nom> --database postgres.

Tests — npm test est ton PREMIER diagnostic

npm test             # unitaires : l'app se CHARGE (imports, décorateurs, config) — < 1 s
npm run test:e2e     # build + boot RÉEL (production --detach --wait) + HTTP + WS + probes

Réflexe : quelque chose semble cassé → npm test AVANT de relire du code ou de redémarrer. En une seconde il prouve que l’app s’importe, que les décorateurs compilent et que la config valide — ou te donne le fichier exact qui casse.

Le rapport est franc : les e2e ont leur propre config (vitest.e2e.config.ts), npm test n’affiche jamais de tests « skipped » qui semblent verts sans avoir rien prouvé. Le test e2e utilise le lancement détaché natif du framework : --wait ne rend la main que quand la readiness est sondée (aucun sleep arbitraire), et nodefony stop arrête proprement. Le client WebSocket est le WebSocket natif de Node — zéro dépendance de test.

Qualité du code

npm run typecheck    # tsgo — le bundler ne type-check PAS : gate séparé, obligatoire
npm run doctor        # cohérence du projet : config, modules déclarés, wiring
npm run lint         # oxlint — garde-fous en warn, non-intrusif
npm run format       # prettier — le style, c'est lui qui décide

Le lint et le typecheck sont deux gates distincts, et c’est volontaire : tsgo juge les TYPES, oxlint juge tout ce qu’un type ne dit pas — code mort, promesse mal formée, import Node sans son préfixe. Aucun des deux ne remplace l’autre.

oxlint s’appuie sur le même analyseur que le bundler (rolldown) : un seul lecteur de ta syntaxe, donc aucun risque que l’un accepte ce que l’autre refuse. Il n’a pas besoin du paquet typescript — celui-ci ne reste dans les dépendances que pour ton éditeur.

Les règles sont dans .oxlintrc.json, commentées par leur intention. Deux familles ne pardonnent pas : le code mort (il finit par mentir) et le préfixe node: sur les modules du runtime (sans lui, un paquet npm homonyme peut prendre la place d’un module natif). Le reste avertit sans bloquer.

Quand ça casse

Dans l’ordre — chaque étape isole un étage, du moins cher au plus cher :

  1. npm test — l’app se charge-t-elle ? Import cassé, décorateur, config invalide : le fichier fautif est nommé.
  2. npm run typecheck — un vert vitest ne type-check RIEN (les types sont effacés à la transpilation) ; tsgo attrape ce que le build laisse passer.
  3. npx nodefony status — un serveur tourne-t-il déjà ? (port occupé, vieux process détaché). npx nodefony stop arrête proprement. Si le port est tenu par une AUTRE application Nodefony, status la nomme : npx nodefony stop <nom> l’arrête sans changer de dossier.
  4. Rebuild — comportement fantôme après un gros changement : npm run build puis relance (le serveur charge dist/, pas tes sources).

npm install s’arrête sur « gyp ERR! find Python »

Ton manifeste porte ceci, et il faut l’y laisser :

"allowScripts": { "better-sqlite3": false }

better-sqlite3 (le pilote SQLite, tiré par @nodefony/drizzle) livre ses binaires déjà compilés et demande à npm de ne pas le recompiler. npm oublie cette demande dès qu’un package-lock.json existe — donc dès ton premier npm install — et lance alors node-gyp rebuild, qui exige Python et un compilateur C++ (npm/cli#9837). La ligne ci-dessus le lui interdit explicitement, ce qu’il respecte dans toutes les versions.

Si tu l’as retirée et que l’installation casse : remets-la, supprime node_modules, relance npm install. Rien à compiler, SQLite fonctionne.

Production (cloud-native)

npm run build        # backend (rolldown) + frontend (vite → public/dist, fingerprinté)
npm start            # nodefony production — bind 0.0.0.0, logs stdout, probes /livez /readyz

Plusieurs processus. Par défaut, un seul process Node par pod : la mise à l’échelle est le travail de l’orchestrateur. Sur une machine dédiée, trois voies règlent le nombre de workers, de la plus forte à la plus faible : npx nodefony production --workers <n|auto> (ou npx nodefony cluster) > NF_WORKERS=<n|auto> (déclarée dans env.ts) > nodefony/config/cluster/cluster.config.ts. Ce fichier n’est pas généré : sa valeur serait le défaut (workers: 1), et chaque application porterait un fichier qui ne dit rien. Crée-le si la topologie doit vivre en git :

// nodefony/config/cluster/cluster.config.ts — lu par le maître AVANT le boot,
// donc sans le moindre import du kernel.
import type { IClusterConfig } from "nodefony";
export default { workers: "auto" } satisfies IClusterConfig;

auto = un worker par cœur alloué (quota cgroup du conteneur), jamais os.cpus(). npm run dev ignore ce réglage : le développement est toujours mono-process.

Le front de production est un build Vite figé (public/dist/), servi en statics par Nodefony — npm run build le produit (il chaîne nodefony frontend:build). Si tu lances npm start sans build, le boot le construit une fois pour toi quand Vite est installé (poste de dev) et le DIT dans les logs ; dans une image de production sans devDependencies, builde à l’image — sinon la page est servie sans interface et le boot le signale en ERROR.

Image de container

Dockerfile et .dockerignore sont générés avec l’app ; la doctrine y est commentée ligne à ligne (multi-stage, USER 1000:1000, sonde sur /readyz, forme exec du CMD).

docker build -t nodefony .
docker run -p 5151:5151 -v nodefony-var:/app/var nodefony
docker stop -t 20 <container>   # SIGTERM → drain → exit 0

🔴 Le -v n’est pas optionnel si tu veux garder tes données. L’app persiste par défaut en sqlite, dans var/databases/ : sans volume, comptes, sessions, jetons et passkeys disparaissent au premier docker rm, sans aucun message. Le volume nommé les met hors du conteneur.

Et tant que la base est sqlite, une seule réplique : c’est un fichier, pas un serveur. Deux conteneurs sur le même volume se corrompent mutuellement ; deux conteneurs sur deux volumes travaillent sur deux bases divergentes, ce que rien ne signale. Pour répliquer, passer à PostgreSQL ou MySQL — il suffit de poser NF_DATABASE_URL, l’ORM déduit le dialecte du scheme et rien d’autre ne change dans l’app.

⚠️ La période de grâce doit rester au-dessus de shutdownDeadline (15 s par défaut) : docker stop n’attend que 10 s sans -t, et k8s 30 s. En dessous, le drain est coupé par un SIGKILL et les requêtes en vol meurent — sans erreur ni trace, à chaque déploiement.

Le code de l’image appartient à root et le processus tourne en 1000:1000 : l’application ne peut pas réécrire son propre dist/. Seuls tmp/ et var/ lui appartiennent, ce qui est exactement ce dont elle a besoin pour écrire.

Sur quelle base l’image est construite

gcr.io/distroless/nodejs24-debian13, et c’est un choix mesuré — quatre candidates construites avec un contenu identique, scannées, puis démarrées :

Base Image finale Critiques Hautes Node libc Shell dedans
gcr.io/distroless/nodejs24-debian13 416 Mo 0 0 24.22.2 glibc non
node:24-alpine 444 Mo 1 7 24.22.2 musl oui (sh)
node:24-slim 531 Mo 0 13 24.22.2 glibc oui (bash)
gcr.io/distroless/nodejs24-debian12 — ne démarre pas 404 Mo 4 33 24.14.0, sous le plancher glibc non

Distroless ne porte que Node et les bibliothèques dont il a besoin : ni shell, ni gestionnaire de paquets, ni npm. Les failles d’Alpine viennent de paquets que ton application n’emploie pas — zlib et musl, que seule une nouvelle image Alpine corrige, et le npm qu’elle embarque. debian12, elle, est restée figée en Node 24.14.0, sous le plancher du framework : elle se construit, puis ne démarre pas.

Ce que tu y perds, et c’est le prix de ce zéro : pas de docker exec … sh pour diagnostiquer (l’étiquette :debug de distroless ajoute un shell, le temps d’un diagnostic), pas de RUN apk add dans l’image finale, et node n’est pas sur le PATH. Tout passe donc par Node lui-même :

docker exec <conteneur> /nodejs/bin/node node_modules/nodefony/bin/nodefony orm:migrate:status

Pour la même raison, un lanceur qui change la commande la passe en arguments : args: en Kubernetes (jamais command:, qui remplacerait le Node d’entrée de l’image), command: dans compose, qui ne touche que les arguments.

Pour en changer — vers Alpine (apk add possible) ou node:24-slim (glibc, avec bash) —, le Dockerfile le dit en tête, en quatre gestes : l’étage de construction et l’étage d’exécution changent ensemble (les modules natifs sont compilés pour la libc de l’étage de construction, et le symptôme d’un mélange est au DÉMARRAGE, pas à l’installation), le RUN mkdir des dossiers de données revient, et la sonde appelle node.

Si tu PUBLIES cette image

L’image emporte node_modules élagué : la publier — sur un registre public, ou en la livrant à un tiers — te rend distributeur du code de tes dépendances. MIT, BSD, ISC et Apache-2.0 demandent alors que leur texte et leur copyright accompagnent le code. C’est déjà le cas matériellement : chaque paquet porte son propre fichier de licence dans node_modules, et l’image les emporte avec lui.

Ce qui manque, c’est l’inventaire — npm le produit sans rien installer :

npm sbom --sbom-format spdx --omit=dev > sbom.spdx.json

Un déploiement interne (ton propre registre, ton cluster) n’est pas une distribution : ces obligations ne s’y appliquent pas. Et un inventaire écrit à la main serait faux dès le premier npm install — c’est pourquoi il se régénère plutôt qu’il ne se commite.

Un process Node = un pod/container ; le scaling horizontal vient de l’orchestrateur (k8s, Swarm, Cloud Run…). Studio est chargé en dev seulement (policy: "dev") — pour l’exposer en production, protège /nodefony par une zone firewall puis passe la policy à "mandatory" (la recette est commentée dans nodefony.config.ts).

Ce qu’il y a dans la boîte

Brique Rôle
nodefony Noyau : modules, injection de dépendances, configuration validée au démarrage, journalisation structurée corrélée, CLI — et le client temps réel partagé avec le navigateur
@nodefony/http Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs, flux SSE, sessions, contextes de requête, certificats TLS
@nodefony/framework Routeur, contrôleurs, décorateurs, vues — le modèle de programmation
@nodefony/security · @nodefony/user Pare-feu par zones, authentification, autorisation par rôles, CSRF, audit
@nodefony/realtime Socket à canaux, salons et leurs membres, admission par canal, binaire CBOR, contre-pression, diffusion entre plusieurs instances
@nodefony/ai Porte MCP : les outils de l’application offerts aux agents d’IA, en HTTP avec flux SSE ou en stdio
@nodefony/orm-core Un contrat de dépôt de données, plusieurs moteurs : Drizzle (SQLite, PostgreSQL, MySQL), Mongoose, Redis
@nodefony/frontend Construction et rechargement à chaud des frontends de chaque module
@nodefony/studio Console d’administration

Par défaut, un processus Node égale une instance : le passage à l’échelle revient à l’orchestrateur, et les journaux partent sur la sortie standard. Sans orchestrateur, nodefony cluster -w N démarre N workers isolés, relancés s’ils tombent.

Développer le framework lui-même

Le poste de travail

Une application n’exige que Node. Le dépôt du framework, lui, se teste contre de vrais services et de vrais clients : il lui faut davantage. Chaque outil ci-dessous est appelé par un script ou une suite du dépôt — aucun n’est là par confort.

Outil Pourquoi Linux (Debian, Ubuntu) macOS (Homebrew) Windows
Node 24.16+ ou 26+ le plancher engines ; la CI joue 24 et 26 nvm install 26 nvm install 26 nvm install 26 (nvm-windows)
git, GitHub CLI dépôt, tickets, tableau de bord apt install git gh brew install git gh winget install Git.Git GitHub.cli
Docker et Compose bases, Redis, Keycloak, proxys : sans lui, des suites entières se sautent et comptent comme vertes Docker Engine + extension compose Docker Desktop Docker Desktop (WSL 2)
jq, ripgrep scripts du dépôt, contrôle des tickets (rg doit être sur le PATH) apt install jq ripgrep brew install jq ripgrep winget install jqlang.jq BurntSushi.ripgrep.MSVC
bun, pnpm, yarn une application générée s’installe avec chacun npm i -g bun pnpm yarn idem idem
mkcert certificat de développement approuvé (sinon : auto-signé) apt install mkcert libnss3-tools brew install mkcert nss winget install FiloSottile.mkcert
gitleaks garde anti-secrets binaire publié brew install gitleaks winget install Gitleaks.Gitleaks
wrk, h2load bancs de charge HTTP/1.1 et HTTP/2 apt install wrk nghttp2-client brew install wrk nghttp2 sous WSL 2
grpcurl interopérabilité gRPC binaire publié brew install grpcurl winget install fullstorydev.grpcurl

npm run test:all -- --infra dit ce qui manque au poste avant de lancer quoi que ce soit, et chaque suite qui saute faute d’un service le nomme en fin de passe.

Cloner et construire

Pour travailler sur le framework, c’est ce dépôt qu’on clone :

git clone https://github.com/nodefony/nodefony-core.git
cd nodefony-core
npm install && npm run build
npm run dev

Une application peut alors suivre ce checkout plutôt que npm : nodefony create app <nom> --link lie ses dépendances nodefony/@nodefony/* en file: — tu modifies le framework, tu le rebâtis, l’application le voit.

État du projet

Le cœur — serveurs, routage, sécurité, temps réel, persistance, console d’administration, construction des frontends — est couvert par des suites de tests exécutables sur infrastructure réelle (npm run test:all), et le dépôt versionne des seuils de fuite mémoire et de charge opposables à chaque exécution.

Ce que ça donne en charge. À travail égal — mêmes journaux, même contexte de requête, mêmes en-têtes de sécurité, même protection CSRF —, Nodefony se tient dans la même zone qu’Express et NestJS munis des mêmes intergiciels : entre 93,6 et 112,7 % de leur débit sur une route qui ne fait rien (21 686 requêtes/s, p99 4,4 ms sur la machine de référence), entre 95,9 et 96,7 % sur une route qui lit et écrit une base — pour une résolution de banc de 3 %. Quatre-vingt-huit minutes de charge continue et 97,5 millions de requêtes laissent le tas, l’empreinte mémoire et le rss plats : 0,021 Mo par million de requêtes. La page de mesures de la version courante — peut-on partir en production ? — porte le comparatif, la tenue dans la durée, le dimensionnement d’un pod et un calculateur ; elle nomme aussi ce que ces chiffres ne prouvent pas. Le dossier docs/performance/ donne le protocole, les scripts qui rejouent chaque chiffre et les instruments qui ont menti avant qu’on s’en aperçoive — aucun absolu pris derrière un chemin virtualisé n’est transposable.

Ce qu’il faut savoir avant de bâtir dessus : les versions JavaScript historiques (≤ 7.x) ne reçoivent plus de correctifs.

Une seule personne en répond, et le projet est écrit pour être repris. Nodefony est libre et développé bénévolement par son auteur. Si l’auteur s’arrête, la connaissance ne part pas avec lui : chaque décision d’architecture est consignée dans un ADR, la documentation renvoie aux symboles du code source et un contrôle refuse un renvoi devenu faux, et le dépôt porte ses consignes pour agents, ses tests et sa chaîne de publication. Ce qui dépend vraiment d’une personne, c’est la responsabilité : publier un correctif, trancher une rupture. La gouvernance le dit sans détour, avec ce qui protège ceux qui s’en servent.

Licence Apache 2.0 — permissive, avec concession de brevet explicite. Christophe Camensuli · [email protected]


La suite est une couche d’agents IA construite sur ce socle : le même pipeline, la même sécurité, le même temps réel — et une application qui sait déjà se décrire à une machine.

D'où ça vient — de 2017 à la réécriture

Nodefony est publié depuis 2017, en JavaScript, et a mûri jusqu'à sa version 7 : des applications réelles tournent encore dessus. Fin 2023, plutôt qu'une migration par petits pas, le choix a été fait d'une réécriture complète en TypeScript — en gardant les concepts, en jetant tout le reste.

Pourquoi ? Parce qu'un framework ne se contente pas d'exécuter du code : il contraint celui qu'on écrit contre lui. Une couche de types posée après coup décrit ce que le code fait ; elle ne garantit rien. Chaque parti pris de la réécriture ferme une porte :

DécisionCe qu'elle rend impossible
TypeScript strict, zéro anyqu'un contrat se dégrade en silence entre deux modules
ESM exclusivementla double résolution CommonJS/ESM et ses pièges de chargement
Décorateurs plutôt que conventionqu'une route existe sans être déclarée là où on la lit
Configuration validée au démarragequ'une clé mal orthographiée soit ignorée sans un mot
Un processus = une instancela supervision maison ; l'échelle revient à l'orchestrateur
La version 10 est l'aboutissement de cette réécriture. Ce n'est pas un portage : c'est le même projet, repensé pour ce que Node.js et TypeScript sont devenus.

Le pari : un transport n'est pas une architecture

La plupart des piles traitent le WebSocket comme une annexe : une seconde table de routes, une seconde pile d'authentification, une passerelle entre les deux. Nodefony refuse cette séparation. La pseudo-méthode WEBSOCKET est un verbe comme GET :

@controller("/api/blog")
class BlogController extends Controller {
  @route("blog-index", { path: "", method: "GET" })
  async index(@CurrentUser() user?: { identifier?: string }) {
    return this.renderJson({ hello: "blog", who: user?.identifier ?? "anonyme" });
  }

  // Même classe, même décorateur : seul le transport déclaré change.
  @route("blog-echo", { path: "/echo", requirements: { methods: ["WEBSOCKET"] } })
  async echo(message: string | Buffer | null) {
    return this.renderJson({ echo: message?.toString() ?? null });
  }
}
Ce que ça change. Une règle d'autorisation protège l'action quel que soit le transport ; la session ouverte en HTTP est celle que voit la socket ; le contexte asynchrone — identifiant de requête, utilisateur — reste stable du handshake à la fermeture. Pas de passerelle, pas de logique dupliquée, pas de seconde surface à sécuriser.

L'isomorphisme — et pourquoi le cœur reste en TypeScript

Le client n'est pas une bibliothèque à part, publiée séparément et rattrapée à chaque version : il vit dans le même paquet que le serveur et partage ses types. Le contrat d'un canal, la forme d'un message, la hiérarchie des rôles s'écrivent une fois.

Sous-cheminCe qu'il donne au navigateur
nodefony/clientsocket temps réel : canaux, appels de service, reconnexion
nodefony/reacthooks (useNodefony, état de connexion, identité)
nodefony/rolesévaluation d'autorisation avec la règle exacte du serveur
nodefony/debugbarbarre de diagnostic branchée sur le contexte de la requête

Un bouton masqué faute de rôle l'est par la même logique que celle qui refusera l'appel. Il n'y a pas deux vérités à synchroniser.

Pourquoi ne pas réécrire le cœur dans un langage plus rapide ? Parce que l'isomorphisme se paierait exactement là. Un portage ferait gagner des microsecondes et perdrait la seule chose qu'un framework fullstack peut vraiment offrir : un contrat unique, vérifié par le compilateur, du contrôleur jusqu'au composant. Ce qui coûte cher dans une application temps réel, ce n'est pas le langage — c'est la frontière entre deux mondes qui doivent se redire la même chose, et qui finissent toujours par diverger. Nodefony supprime la frontière au lieu d'optimiser le passage.

Prêt pour les agents — ce qui tranche pour la suite

Une application est aujourd'hui écrite à deux mains : la personne, et l'agent qu'elle pilote. Un agent lâché dans un projet bâti sur un framework qu'il connaît mal invente — et il invente du plausible : un CRUD écrit à la main là où un générateur existait, un import direct du driver de base qui contourne la façade, une socket bas niveau là où le framework offre un canal. Ce code compile. Il passe même les tests. Il a vieilli avant d'être relu.

La réponse n'est pas un assistant intégré, c'est de rendre l'application capable de se décrire :

Ce qui est poséCe que ça évite
Un AGENTS.md généré à la racinela convention périmée : le fichier est dérivé du projet réel, il ne peut pas mentir
La documentation voyage dans les paquetsl'agent qui cherche sur le web une version qui n'est pas la vôtre
Un catalogue des briques publiéle choix de module fait au jugé, sans savoir ce qu'un adaptateur ne couvre pas
inspect · check · envla déduction depuis le code : routes, services, configuration effective et sa provenance
Des générateurs pilotables en JSONl'imitation d'un fichier d'exemple au lieu de l'appel à l'outil qui produit le vrai code
Un graphe symbolique du codela fouille par recherche textuelle : qui étend quoi, qui implémente quoi, en une lecture
Le standard retenu — AGENTS.md — est celui que lisent la plupart des outils de codage, avec la règle « le plus proche gagne ». Rien de propriétaire : c'est un index court qui pointe vers la documentation installée au lieu de la recopier. Une règle recopiée dérive ; une règle pointée reste vraie.
Et surtout : c'est MESURÉ. Une application témoin est générée, puis un agent y reçoit des tâches réelles — ajouter un CRUD, protéger une route, écrire une commande, configurer par l'environnement, choisir la bonne brique. Le harnais lit le transcript et le code produit pour répondre à une seule question : a-t-il lu, ou deviné ? La métrique n'est pas « le code marche » — il marche souvent, c'est tout le piège. Le banc n'est pas entièrement vert aujourd'hui, et chaque échec désigne un endroit précis où l'application ne se rend pas assez évidente. C'est à ça qu'il sert.

Aucun framework backend Node n'offre aujourd'hui d'équivalent officiel. C'est un espace vide, et c'est délibérément là que Nodefony se place.

Ce que les suites éprouvent — les angles, pas seulement le nombre

Un compte de tests ne dit rien de ce qu'ils regardent. Ces chiffres sont comptés dans le dépôt au moment de la génération de cette page, fichier par fichier.

Cas d'attaque
588
40 suites offensives : on essaie de casser ses propres garanties
Intégration
1 530
224 suites contre un serveur qui tourne pour de vrai
Bout en bout
287
52 suites : une application générée, démarrée, interrogée
Charge & mémoire
60
seuils de fuite et de débit, tenus par un gate séparé

S'y ajoutent 12 flux d'intégration continue — analyse statique de sécurité, gate mémoire, suites ORM sur plusieurs moteurs, bout-en-bout autonomes, contrôle avant publication et installation à blanc des paquets depuis leurs archives.

Ce que ces chiffres ne prouvent pas. Un test vert ne vaut que pour ce qu'il exerce : une suite qui se passe de son infrastructure se SKIPPE, et un skip compte comme un vert. C'est pourquoi le dépôt fait échouer une passe d'intégration continue dont une cible déclarée n'a pas été exercée — une absence voulue s'énonce, elle ne s'oublie pas.

La matrice — ce qui est couvert, domaine par domaine

Calculé depuis le dépôt au moment de la génération de cette page : pour chaque domaine, le code, les suites de tests, la documentation destinée aux humains, et la fiche d'entrée du paquet.

Cas de test
15 349
exécutés, répartis sur 1 218 fichiers
Pages de doc
154
dont 67 transverses et 30 décisions d'architecture
Fiches de paquet
15/15
README à jour
Modules de code
1 957
fichiers TypeScript de source
Noyau, injection, configuration, journalisation, CLInodefony747couvert ailleurs17✓
Transports HTTP · HTTPS · HTTP/2 · WebSocket, sessions@nodefony/http349couvert ailleurs11✓
Canaux, appels bidirectionnels, diffusion multi-instances@nodefony/realtime182couvert ailleurs13✓
Pare-feu, authentification, autorisation, audit@nodefony/security15212719✓
Routage, contrôleurs, décorateurs, vues@nodefony/framework151couvert ailleurs8✓
Moteur SQL — SQLite, PostgreSQL, MySQL@nodefony/drizzle841412✓
Console d'administration@nodefony/studio53couvert ailleurs2✓
Construction des frontends, rechargement à chaud@nodefony/frontend50couvert ailleurs1✓
Cache, sessions, diffusion entre instances@nodefony/redis37couvert ailleurs3✓
Contrat de dépôt de données, transactions, critères@nodefony/orm-core35212✓
Socle utilisateur, encodeurs, service de comptes@nodefony/user33242✓
Moteur documentaire — MongoDB@nodefony/mongoose30332✓
Portail de documentation@nodefony/documentation21couvert ailleurs2✓
devkit@nodefony/devkit17221✓
ai@nodefony/ai16142✓
Un zéro dans la colonne Tests désigne un paquet dont la couverture vient d'ailleurs (ses consommateurs, ou les suites d'intégration à la racine) — pas une zone non éprouvée. La colonne se trie : c'est fait pour être regardé, pas pour être cru.

La sécurité s'éprouve par l'attaque, pas par la lecture

Le pare-feu découpe l'application en zones, chacune avec sa chaîne d'authentification, et il refuse plutôt que d'ouvrir : une configuration invalide capture le trafic et répond 401.

SurfaceCe qui est en place
Identitéssession serveur pour le web · jetons et clés d'API pour les machines · OAuth2/OIDC · WebAuthn · TOTP
CryptographieArgon2id pour les mots de passe · signatures Ed25519 · secrets chiffrés au repos
DéfensesCSRF par métadonnées de requête · en-têtes de sécurité · limitation de débit · journal d'audit persistant
Autorisationhiérarchie de rôles vérifiée au démarrage, refus par défaut, isomorphe côté navigateur

Équipe rouge, équipe bleue

« Le login fonctionne » ne prouve rien. Les campagnes se mènent en deux passes, et l'ordre est le cœur du dispositif :

  1. Passe rouge — la menace d'abord. La matrice d'attaque est construite depuis les standards et les faiblesses connues avant d'avoir lu le code. C'est un garde-fou contre son propre biais : qui lit l'implémentation en premier ne teste que ce qu'elle prévoit, et rate précisément ce qu'elle a oublié.
  2. Passe bleue — le code ensuite. On lit l'implémentation, on couvre les branches restantes, on regarde les chemins que la passe rouge n'imaginait pas.
  3. Le cycle. Faille trouvée → corrigée → re-prouvée par un test qui échouait avant elle. Un correctif sans son test de non-retour ne compte pas comme corrigé.

Ce qui distingue ces attaques d'un scan générique : beaucoup visent des surfaces propres à cette architecture — les portées de l'injection de dépendances, les messages du pipeline WebSocket partagé, le jeton porté par le contexte asynchrone, les zones du pare-feu et leurs contournements. Aucun outil sur étagère ne connaît ces surfaces : il faut concevoir les attaques.

À savoir avant de concevoir une application. Le refus par défaut opère par zone du pare-feu, pas route par route : une route hors de toute zone déclarée est publique. Déclarez vos zones.
Deux disciplines qui vont plus loin que l'usage
  • Un test qu'on n'a jamais vu échouer ne prouve rien. Un test neuf est débranché volontairement une fois, pour vérifier qu'il tombe. Sinon il est complaisant par construction.
  • L'historique du dépôt est scanné à la recherche de secrets — pas seulement l'état courant : passer un dépôt en public expose chaque révision.

Le framework se regarde tourner

Une console d'administration est livrée avec le framework : topologie du runtime, journaux en direct avec rejeu, suivi d'une requête de bout en bout par son identifiant, schéma de la base, graphe des classes par module, gouvernance de la sécurité, et les générateurs de code pilotables à la souris — la sortie diffusée comme un terminal.