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.
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
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.
📖 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.mddé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 decreate 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,leaveScopele 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 chaqueawait, 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 (ouundefined),requireScope()lève en nommant la cause. La bulle ALS de la requête dit lequel est le tien. Un hookonAfterResponsele 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://:nodefony-dev@127.0.0.1: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 :
npm test— l’app se charge-t-elle ? Import cassé, décorateur, config invalide : le fichier fautif est nommé.npm run typecheck— un vert vitest ne type-check RIEN (les types sont effacés à la transpilation) ;tsgoattrape ce que le build laisse passer.npx nodefony status— un serveur tourne-t-il déjà ? (port occupé, vieux process détaché).npx nodefony stoparrête proprement. Si le port est tenu par une AUTRE application Nodefony,statusla nomme :npx nodefony stop <nom>l’arrête sans changer de dossier.- Rebuild — comportement fantôme après un gros changement :
npm run buildpuis relance (le serveur chargedist/, 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 buildle produit (il chaînenodefony frontend:build). Si tu lancesnpm startsans 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
-vn’est pas optionnel si tu veux garder tes données. L’app persiste par défaut en sqlite, dansvar/databases/: sans volume, comptes, sessions, jetons et passkeys disparaissent au premierdocker 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 stopn’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 · ccamensuli@gmail.com
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écision | Ce qu'elle rend impossible |
|---|---|
TypeScript strict, zéro any | qu'un contrat se dégrade en silence entre deux modules |
| ESM exclusivement | la double résolution CommonJS/ESM et ses pièges de chargement |
| Décorateurs plutôt que convention | qu'une route existe sans être déclarée là où on la lit |
| Configuration validée au démarrage | qu'une clé mal orthographiée soit ignorée sans un mot |
| Un processus = une instance | la supervision maison ; l'échelle revient à l'orchestrateur |
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 });
}
}
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-chemin | Ce qu'il donne au navigateur |
|---|---|
nodefony/client | socket temps réel : canaux, appels de service, reconnexion |
nodefony/react | hooks (useNodefony, état de connexion, identité) |
nodefony/roles | évaluation d'autorisation avec la règle exacte du serveur |
nodefony/debugbar | barre 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.
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 racine | la convention périmée : le fichier est dérivé du projet réel, il ne peut pas mentir |
| La documentation voyage dans les paquets | l'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 · env | la déduction depuis le code : routes, services, configuration effective et sa provenance |
| Des générateurs pilotables en JSON | l'imitation d'un fichier d'exemple au lieu de l'appel à l'outil qui produit le vrai code |
| Un graphe symbolique du code | la fouille par recherche textuelle : qui étend quoi, qui implémente quoi, en une lecture |
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.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.
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.
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.
| Noyau, injection, configuration, journalisation, CLI | nodefony | 747 | couvert ailleurs | 17 | ✓ |
| Transports HTTP · HTTPS · HTTP/2 · WebSocket, sessions | @nodefony/http | 349 | couvert ailleurs | 11 | ✓ |
| Canaux, appels bidirectionnels, diffusion multi-instances | @nodefony/realtime | 182 | couvert ailleurs | 13 | ✓ |
| Pare-feu, authentification, autorisation, audit | @nodefony/security | 152 | 127 | 19 | ✓ |
| Routage, contrôleurs, décorateurs, vues | @nodefony/framework | 151 | couvert ailleurs | 8 | ✓ |
| Moteur SQL — SQLite, PostgreSQL, MySQL | @nodefony/drizzle | 84 | 141 | 2 | ✓ |
| Console d'administration | @nodefony/studio | 53 | couvert ailleurs | 2 | ✓ |
| Construction des frontends, rechargement à chaud | @nodefony/frontend | 50 | couvert ailleurs | 1 | ✓ |
| Cache, sessions, diffusion entre instances | @nodefony/redis | 37 | couvert ailleurs | 3 | ✓ |
| Contrat de dépôt de données, transactions, critères | @nodefony/orm-core | 35 | 21 | 2 | ✓ |
| Socle utilisateur, encodeurs, service de comptes | @nodefony/user | 33 | 24 | 2 | ✓ |
| Moteur documentaire — MongoDB | @nodefony/mongoose | 30 | 33 | 2 | ✓ |
| Portail de documentation | @nodefony/documentation | 21 | couvert ailleurs | 2 | ✓ |
| devkit | @nodefony/devkit | 17 | 22 | 1 | ✓ |
| ai | @nodefony/ai | 16 | 14 | 2 | ✓ |
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.
| Surface | Ce qui est en place |
|---|---|
| Identités | session serveur pour le web · jetons et clés d'API pour les machines · OAuth2/OIDC · WebAuthn · TOTP |
| Cryptographie | Argon2id pour les mots de passe · signatures Ed25519 · secrets chiffrés au repos |
| Défenses | CSRF par métadonnées de requête · en-têtes de sécurité · limitation de débit · journal d'audit persistant |
| Autorisation | hié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 :
- 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é.
- 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.
- 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.
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.
Chaque requête porte un identifiant propagé dans tout le pipeline : la console rejoue son trajet complet — phases, requêtes de base de données, décisions du pare-feu.
Même principe pour les générateurs de code : ils publient leur catalogue, montrent le plan et le diff avant d'écrire quoi que ce soit, et acceptent leurs réponses par fichier. Un refus ne laisse rien derrière lui.
Performance : la question honnête
Le framework fait à chaque requête ce qu'une pile minimale ne fait pas : contexte asynchrone corrélé, identifiant de requête, en-têtes de sécurité, contrôle CSRF, résolution de zone de pare-feu. Comparer sans ce travail ne compare rien. Les constantes ci-dessous servent à dimensionner, pas à gagner un concours.
Ce que le dépôt garantit à chaque commit
| Barrière | Ce qu'elle empêche |
|---|---|
| Typage strict, sur tous les espaces de travail | aucun avertissement du compilateur mis sous le tapis ; les exceptions de type ne vivent que dans les tests, où elles servent d'assertions |
| Seuils de mémoire et de charge versionnés | une fuite ou une régression de latence passe en échec, pas en discussion |
| Suites exécutables sur infrastructure réelle | un test sauté faute de base compte comme vert : le lanceur dit ce qu'il n'a pas exercé |
| Graphe symbolique régénéré à chaque commit | un renommage silencieux, une relation cassée entre modules |
| Intégration continue multi-systèmes | le « ça marche chez moi » sur une seule plateforme |
Les preuves existent, et elles sont rejouables
Chaque affirmation de ce support s'adosse à un artefact produit par le dépôt lui-même, pas à une déclaration d'intention. Ces rapports se régénèrent par une commande :
| Artefact | Ce qu'il établit |
|---|---|
| Rapport de capacité | les constantes de dimensionnement d'une instance, et le nombre d'instances pour une charge donnée |
| Banc de charge HTTP et WebSocket | le débit soutenable, les percentiles, le point de rupture |
| Audit du pipeline de requête | où passe le temps, phase par phase, profileur éteint |
| Scan de l'historique | aucun secret dans les révisions du dépôt, pas seulement dans l'état courant |
| Banc du générateur de code | le code produit compile, ses tests passent, sa ressource répond réellement en HTTP |
| Registre des écarts documentation ↔ code | chaque page de référence est confrontée au code qu'elle décrit |
Ce qui manque — dit avant qu'on le demande
| Manque | Portée réelle |
|---|---|
| Couche d'agents IA | seule la porte MCP est livrée (@nodefony/ai : exposer les outils de l'application à un agent) ; modèle, mémoire, recherche augmentée et boucle d'agent restent une vision — rien dans la version courante n'en dépend |
| Console d'administration sans tests de composants | ses écrans sont couverts par le typage, des tests unitaires de leur logique et une sonde navigateur sur deux pages en intégration continue — aucun composant n'est éprouvé en rendu |
| Projet à un seul contributeur | bénévole ; les délais sont dits, pas promis (politique de sécurité), et seule la dernière version majeure reçoit des correctifs (compatibilité) |
Dire les manques n'est pas une précaution rhétorique : c'est ce qui rend le reste vérifiable. Un dossier sans zone d'ombre décrit rarement un vrai logiciel.
La direction
La suite est une couche d'agents 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.
Rien de cette couche n'est promis dans la version courante. Ce qui est acquis, en revanche, c'est le terrain sur lequel elle se posera : un plan de données auto-décrit, un graphe symbolique du code, un générateur pilotable par fichier, et un transport duplex qui fait du flux un cas ordinaire plutôt qu'un montage.