Architecture technique

Le Labo du Yeti est un CMS HTML headless mono-instance, conçu pour le modèle SaaS hybride 1 client = 1 instance Plesk. Cette page décrit la stack complète, les couches du système, les flux de requêtes et les choix d'architecture qui ont structuré le projet. Tous les composants ci-dessous sont en production et fonctionnent ensemble dans un seul processus Node.js par instance client.

Vue d'ensemble. Backend Node 22 LTS (ESM) + Express 4, base SQLite locale (better-sqlite3 WAL), frontend React 18 buildé par Vite et servi statiquement, rendu public en HTML pur via un pipeline Cheerio. Aucun service externe obligatoire à part SMTP pour les emails et l'API IA pour les générations.

Philosophie d'architecture

Le projet répond à quatre contraintes structurantes :

  1. Mono-instance par client — chaque domaine tourne son propre processus Node, sa propre DB, son propre JWT_SECRET. Pas de mutualisation, donc pas de fuite cross-tenants possible.
  2. Plesk-compatible — la stack ne dépend que de Node + npm. Pas de Docker, pas de Redis, pas de Postgres. La DB est un fichier content/cms.db embarqué.
  3. Sortie 100 % HTML statique — le public final n'exécute aucun JavaScript serveur. Le rendu écrit des fichiers dans public/, qui sert de DocumentRoot Plesk.
  4. Extensible — un système d'extensions WordPress-like permet d'ajouter routes, hooks, sidebar et pages custom sans toucher au coeur.

Stack backend

Runtime & framework HTTP

ComposantVersionRôle
Node.js22 LTSRuntime ESM ("type": "module"). Choisi pour les prebuilds better-sqlite3 et le support natif fetch.
Express4.xFramework HTTP. Routeur principal monté sous /api, gestion des middlewares en chaîne.
helmetrecentEn-têtes de sécurité (CSP, X-Frame-Options, etc.).
express-rate-limitrecentLimiteurs spécialisés : loginLimiter, aiGenLimiter, aiCallLimiter, submitLimiter, restoreLimiter.
corsrecentOrigine contrôlée par CORS_ORIGIN (env Plesk).

Persistance & sécurité

ComposantRôle
better-sqlite3Driver SQLite synchrone. PRAGMA journal_mode=WAL, synchronous=NORMAL, foreign_keys=ON, busy_timeout=5000.
bcryptjsHash des mots de passe utilisateurs et fallback pur-JS sans dépendance native.
jsonwebtokenJWT signés (JWT_SECRET par instance). Source d'authentification primaire.
multerUpload multipart. Profils dédiés : uploadHtml, uploadImage, uploadMedia, plus un upload ZIP 10 Mo pour les extensions et 200 Mo pour les restaurations.
nodemailer 8SMTP universel (login 2FA, reset password, soumissions de formulaires, rappels cocon).
adm-zipCréation des archives backup/export et inspection des ZIP d'extensions.
sharpTraitement d'images (crop favicon en WebP 512×512, conversion par l'extension webp-auto-converter).

Pipeline HTML

Le coeur du rendu public repose sur Cheerio, un parser HTML jQuery-like côté serveur. Tous les templates uploadés (pages, articles, cocon, layout global) sont lus en mémoire puis transformés en cascade :

Le résultat est écrit en HTML statique dans public/ par le service buildSite(), qui produit aussi sitemap.xml, robots.txt et la page 404 personnalisable. Voir Moteur HTML pour le détail du pipeline.

Stack frontend

Le frontend est une SPA React buildée par Vite, servie depuis frontend/dist/. En production, le backend Express n'expose pas le panel directement : il est monté sous un slug admin aléatoire pour rendre la surface d'attaque moins visible.

ComposantVersionRôle
React18UI principale, hooks-first, pas de class components.
Vite5Bundler dev + prod. Build produit frontend/dist/ commité sur la branche release.
react-router-dom6Routage SPA. Routes protégées par un guard lisant auth-store.
zustandrecentStores d'état globaux légers : auth-store (user, token, capabilities), theme-store (light/dark, accent).
Tailwind CSS3Utility-first sur des design tokens CSS centralisés dans frontend/src/styles/tokens.css.
axiosrecentClient HTTP avec intercepteurs JWT et gestion globale des 401/403.

Design tokens

Toutes les couleurs, espacements et radius passent par des variables CSS définies dans tokens.css. Tailwind est configuré pour les consommer via theme.extend. Cela permet :

Règle non-négociable. Les composants React ne doivent JAMAIS hard-coder des couleurs. Toujours passer par les classes Tailwind alimentées par les tokens. Voir conventions UI.

Flux d'une requête HTTP

Une requête authentifiée vers l'API suit toujours la même chaîne de middlewares. Exemple type pour POST /api/blog/articles :

Client (axios)
  │
  ▼
helmet                         ← en-têtes de sécurité (CSP, HSTS, XFO)
  │
  ▼
cors                           ← validation Origin contre CORS_ORIGIN
  │
  ▼
express.json() / multer        ← parsing body ou multipart
  │
  ▼
rateLimit (loginLimiter,       ← protection brute-force / spam IA
           aiGenLimiter, ...)
  │
  ▼
/api router                    ← dispatch vers le sous-router (auth, blog, …)
  │
  ▼
requireAuth                    ← extrait JWT ou token API, charge req.user
  │
  ▼
requireRole('owner','admin',   ← vérifie l'appartenance au set de rôles
            'webmaster',
            'editor')
  │
  ▼
requireCapability('blog')      ← matrice rôle × feature (SQLite)
  │
  ▼
controller (blog-controller)   ← validation payload, appel service
  │
  ▼
service (blog-service)         ← logique métier, orchestrations IA
  │
  ▼
db-store (better-sqlite3)      ← INSERT/UPDATE transactionnel
  │
  ▼
Réponse JSON                   ← status + body sérialisé

Chaque couche a une responsabilité unique :

CoucheResponsabilitéNe fait jamais
helmet / corsSécurité réseau, en-têtes HTTP.Logique métier.
rateLimitQuotas par IP sur les endpoints sensibles.Authentification.
requireAuthDécodage JWT ou validation token API, peuplement req.user.Décisions d'accès fines.
requireRoleVérification dure du rôle utilisateur.Vérifications par feature.
requireCapabilityVérification dynamique de la matrice persistée.Bypass owner (owner court-circuite cette étape).
controllerValidation du payload HTTP, traduction REST → service.Accès direct à la DB.
serviceRègles métier, orchestrations multi-tables, appels IA.Manipulation du req/res.
db-storeHelpers SQL préparés, transactions.Logique métier.
Pourquoi ce découpage ? Les services sont testables en isolation, les controllers restent fins, et le middleware requireCapability permet de désactiver une feature pour un rôle sans toucher au code (matrice éditable via /api/capabilities).

Flux du rendu d'une page publique

Quand un build est déclenché par POST /api/build, le service build-service orchestre le rendu de toutes les pages, articles et pages cocon. Pour chaque entité, le pipeline est :

  1. Lecture du layout global — le HTML actif est lu depuis uploads/layouts/<active>.html.
  2. Lecture du template de la page — le HTML uploadé via POST /api/pages ou généré par l'IA pour les articles/cocon.
  3. Appel render-service qui :
    • Parse layout et page avec cheerio.load().
    • Injecte les data-cms (texte, href, src, alt, classes) depuis page.overrides et les helpers globaux (header/footer).
    • Injecte les data-ai (titres, paragraphes, FAQ, JSON-LD) depuis le contenu généré.
    • Compose : insère la page dans le slot <main data-cms-slot="content"> du layout.
    • Génère le bloc SEO (title, meta, Open Graph, canonical, hreflang, JSON-LD).
    • Réécrit les URLs des médias vers ./assets/media/ relatifs à la position du fichier final.
  4. Hook render.html — toute extension peut transformer le HTML final (ex. surefeedback-connector injecte un snippet avant </body>).
  5. Écriturefs.writeFileSync() vers public/<slug>/index.html (ou public/<parent>/<enfant>/index.html pour les cocons enfants).
  6. Génération sitemap — collecte de toutes les URLs publiées, écriture de public/sitemap.xml et public/robots.txt.

Voir Moteur HTML (pipeline) pour le détail du Cheerio, et Build & Publication pour l'orchestration.

Schedulers internes

Deux schedulers tournent dans le processus Node principal. Ils s'appuient sur setInterval avec une garde au boot pour éviter les doubles exécutions en cas de redémarrage rapide.

Notifications cocon

Toutes les 5 minutes, le scheduler scanne les cocon_groups dont le quota anti-spam vient de se débloquer. Si un créneau est libre depuis moins de 5 minutes, un email est envoyé via SMTP aux utilisateurs ayant les rôles webmaster ou chef_projet (SLOT_NOTIFICATION_ROLES). Voir Cocon SEO.

Publication blog programmée

Le scheduler blog scanne les articles avec status='scheduled' et scheduled_at <= NOW(). Il bascule leur statut en published et déclenche un buildSite() ciblé sur l'article et l'index blog. Cela permet la publication différée sans intervention manuelle. Voir Blog IA.

Pas de cron externe. Tout tourne dans le process Node. Si l'instance est redémarrée par Plesk (Phusion Passenger), les schedulers reprennent au prochain démarrage. Aucune dépendance à un Redis, à un Bull, ni à un cron système.

Synthèse des couches

CoucheFichiers typesDépendances autorisées
HTTP / middlewarebackend/src/middleware/*.jsexpress, helmet, jwt, db-store (lecture only)
Routesbackend/src/routes/*.jscontrollers, middleware
Controllersbackend/src/controllers/*.jsservices, validation
Services métierbackend/src/services/*.jsdb-store, autres services, cheerio
Persistancebackend/src/utils/db-store.jsbetter-sqlite3 uniquement
Pipeline rendubackend/src/services/render-service.jscheerio, fs, db-store
Extensionsextensions/installed/<id>/index.jscontexte ext.* injecté (sandbox)

Cette discipline de couches est ce qui rend le code maintenable malgré l'absence de framework lourd type Nest ou Adonis. Chaque ajout de feature passe par les mêmes étapes : route → controller → service → db-store, plus éventuellement un hook d'extension. Voir Structure des dossiers pour la cartographie complète.

Ce que l'architecture évite volontairement

Pour aller plus loin, lire ensuite Structure des dossiers qui détaille l'arborescence physique du repo, puis Base de données pour le schéma SQLite complet.