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

Stockage de session — où vivent les sessions, et comment en écrire un

stablemis à jour 2026-09-01

Comment Nodefony choisit où ranger les sessions HTTP/WS, quels backends existent, et comment en écrire un sur mesure. Le mécanisme est en inversion de contrôle : @nodefony/http ne dépend d’aucun ORM — chaque backend s’enregistre lui-même.

📍 DocumentationGuidesStockage de session

ℹ️ Modèle d’identité hybride : le web et la console d’administration s’appuient sur une session serveur (cookie opaque, révocable) — c’est la fondation, pas une dette ; le JWT est réservé aux API et aux agents sans état (machine à machine). La session reste donc pleinement supportée. Vue d’ensemble de TOUTE la persistance : persistence.md.

Le modèle — inversion de contrôle#

SessionsService tient un registre statique de constructeurs de stockage, et n’importe aucun ORM. Chaque module fournisseur s’y inscrit lui-même au moment où il est chargé.

@nodefony/http : SessionsService.storages : Map<name, StorageCtor>
   ▲ registerStorage("memory",   MemorySessionStorage)  ← http lui-même (intégré)
   ▲ registerStorage("drizzle",  SessionStorage)        ← @nodefony/drizzle au chargement
   ▲ registerStorage("mongoose", SessionStorage)        ← @nodefony/mongoose au chargement
   ▲ registerStorage("redis",    RedisSessionStorage)   ← @nodefony/redis au chargement

initializeStorage() → SessionsService.getStorage(nom) → new Storage(this)

Deux bénéfices, et c’est toute la raison d’être du mécanisme : aucun cycle http ↔ ORM, et ajouter un pilote ne touche pas @nodefony/http. La résolution du nom est insensible à la casse.

Le registre et sa résolution vivent dans SessionsService.registerStorage() (sessions-service.ts:182), SessionsService.getStorage() (sessions-service.ts:191) et SessionsService.storageHandlers() (sessions-service.ts:196) ; le stockage intégré s’enregistre en fin de fichier (sessions-service.ts:886).

Choisir le backend — un seul réglage#

La clé session.store du module http sélectionne le stockage par son nom (config.ts:774) :

// nodefony/config/modules/http-config.ts (surcharge applicative)
export default {
  session: {
    store: "auto", // "auto" | "memory" | "drizzle" | "mongoose" | "redis"
  },
};
Nom Fourni par Ce que ça range
auto — (défaut) Ne choisit pas : suit l’infrastructure déclarée
memory @nodefony/http En mémoire du process — volatil (développement, banc)
drizzle @nodefony/drizzle Table session, via orm-core
mongoose @nodefony/mongoose Collection MongoDB
redis @nodefony/redis Clés Redis, avec expiration native

Un backend n’est disponible que si le module qui le fournit est chargé (@modules()). store: "drizzle" exige donc @nodefony/drizzle dans @modules().

Ce que fait auto, précisément#

C’est le défaut, et il ne devine rien : il lit l’infrastructure déclarée de l’application et prend le premier moyen réellement disponible — cache Redis, puis base de données, puis SQLite local si drizzle est chargé, et à défaut memory. Le choix est borné aux stockages réellement enregistrés, et il est annoncé dans les journaux au boot :

session.store "auto" → "redis" (cache redis déclaré)
SESSION STORAGE active : redis

La résolution est portée par resolveAutoStore() (infra.ts:241), appelée depuis initializeStorage() (sessions-service.ts:239) — la même fonction sert aux autres briques qui déclarent un store, ce qui évite deux politiques divergentes.

L’interface du registre#

import { SessionsService } from "@nodefony/http";

SessionsService.registerStorage("mybackend", MyStorage); // enregistrer
SessionsService.getStorage("drizzle"); // constructeur | undefined
SessionsService.storageHandlers(); // ["memory", "drizzle", …]

Événements (observabilité)#

Émis sur le kernel :

Événement Quand Arguments
onRegisterSessionStorage un backend s’enregistre (name, ctor)
onSessionStorageReady le stockage actif est instancié (handler, storage)
kernel.on("onSessionStorageReady", (handler) => {
  /* afficher le backend de session actif */
});

Écrire un backend sur mesure#

  1. Implémenter le contrat ISessionStorage (@nodefony/http) :
import type { ISessionStorage } from "@nodefony/http";

class S3SessionStorage implements ISessionStorage {
  read(id: string): Promise<unknown> {
    /* … */
  }
  write(id: string, data: unknown, ctx: string): Promise<unknown> {
    /* … */
  }
  start(id: string, ctx: string): Promise<unknown> {
    /* … */
  }
  open(ctx: string): Promise<number> {
    /* … */
  }
  close(): boolean {
    /* … */
  }
  destroy(id: string, ctx: string): Promise<boolean> {
    /* … */
  }
  gc(maxlifetime: number, ctx: string): Promise<void> {
    /* … */
  }
}
  1. L’enregistrer au chargement du module, jamais dans le constructeur d’un service — le registre doit être peuplé avant que la session ne s’initialise :
import { SessionsService } from "@nodefony/http";
SessionsService.registerStorage("s3", S3SessionStorage);
export default S3SessionStorage;
  1. Activer : session: { store: "s3" }. Le module fournisseur doit être dans @modules().

Piège de bundle : appeler SessionsService.registerStorage(...) rend l’import de @nodefony/http une valeur et non plus un import de type — il faut donc ajouter @nodefony/http à la liste external du rolldown.config.ts du module fournisseur, sinon le paquet embarque une seconde copie de SessionsService et s’inscrit dans un registre que personne ne lit.

Référence — backend ORM (exemple Drizzle)#

@nodefony/drizzle montre le patron orm-core : entité session (@entity, table créée au boot), stockage adossé au repository, purge par un opérateur portable { updatedAt: { $lt: cutoff } }. Voir @nodefony/drizzle.

Performance : avec better-sqlite3 (synchrone, connexion unique) le débit d’écriture de session plafonne — les écritures se sérialisent. Un pilote Drizzle PostgreSQL ou MySQL parallélise. La correction est le backend, pas le mécanisme.

📖 Lexique#

Terme Ce que c’est
Stockage (storage) L’objet qui sait lire, écrire, détruire et purger les sessions dans un support donné. Contrat : ISessionStorage.
Registre La table nom → constructeur tenue par SessionsService. Un module fournisseur s’y inscrit ; @nodefony/http n’en connaît aucun.
IoC (inversion de contrôle) Le consommateur ne va pas chercher son fournisseur : c’est le fournisseur qui vient s’annoncer. C’est ce qui casse le cycle.
auto La valeur par défaut de session.store : ne nomme pas un backend, mais délègue le choix à l’infrastructure déclarée.
Purge (gc) Le passage qui supprime les sessions expirées. Hors chemin critique, réglée par session.gcIntervalS.

⚠️ Pièges#

🧪 Tests & couverture#

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

<!-- prettier-ignore -->

Type Ce qui est prouvé
Unitaires @nodefony/http unit/session-facets.test.ts, unit/session-pagination.test.ts, unit/SessionsAdmin.test.ts facettes de session, pagination, surface d’administration
Unitaires (infra) nodefony tests/infra.test.ts la résolution auto suit bien l’infrastructure déclarée
Intégration @nodefony/drizzle session-storage.test.ts, session-store-sqlite.test.ts · @nodefony/mongoose session-storage.test.ts · @nodefony/redis session-store.test.ts, session-resilience.test.ts le comportement de CHAQUE backend, et sa tenue quand le support tombe
E2E (base réelle) @nodefony/drizzle session-store-postgres.e2e.test.ts, session-store-mysql.e2e.test.ts dialectes réels

🛑 Prudence

Les suites E2E se skippent sans leurs variables d’infrastructure, et un skip compte comme vert. Avant de conclure « tout passe » sur PostgreSQL et MySQL, vérifier que NF_PG_URL et NF_MYSQL_URL étaient posées — source unique : vitest.gates.ts à la racine.

🔗 Pour aller plus loin#