Description technique exhaustive du site Deal ex Machina : stack, architecture, qualite du code, CI/CD, securite, confidentialite, RGPD, loi IA europeenne, scores de performance, feuille de route, et pourquoi le web est la demo.
Ce site est la démo. Pas un projet à part avec un portfolio séparé : le site que vous lisez est la preuve de la maîtrise technique, de l’infrastructure et de la sécurité jusqu’aux perfs front et à l’expérience développeur. Il a été entièrement codé à la main avec Cursor [1] (IDE assisté par IA), sans low-code ni constructeurs de pages. Ce qui suit est une description niveau CTO, du bas vers le haut : runtimes, données, APIs, garde-fous qualité, CI/CD, sécurité et les chiffres de performance que nous nous imposons.
Pourquoi pas un site vitrine classique (WordPress, Wix, Squarespace) ? Parce que pour un cabinet technique, le site est le produit : chaque dépendance, chaque API, chaque en-tête et cookie est un engagement. Une vitrine en thèmes et plugins masque précisément ce que nous vendons—l'architecture, la sécurité, la performance—derrière une boîte noire. Ici, pas de thème à incriminer, pas de plugin à patcher : un seul codebase, la main sur tout, typé de la base à l'UI, et un déploiement qu'on peut expliquer ligne à ligne. Le récit tient en une phrase : ce que vous voyez est ce que nous construisons.
Node.js [2] : Figé à >=20.19.6 (et npm >=10.8.2) via les engines du package.json. Nous utilisons la LTS et évitons les majeurs flottants en production.
TypeScript [3] : 5.9.x en mode strict (strict: true, noEmit: true, isolatedModules: true, moduleResolution: "bundler"). Aucun any en production ; alias de chemins @/* pour des imports propres. Le code est en ESM autant que possible (voir les conventions du projet).
Framework : Next.js 16 [4] (App Router). Nous utilisons la sortie standalone pour le déploiement Docker/Koyeb et le mode export statique pour Cloudflare Pages lorsque NEXT_OUTPUT=export ou CLOUDFLARE_PAGES=1. Un seul codebase, deux cibles de déploiement : serveur Node ou statique + edge.
Base de données : PostgreSQL [5] via Drizzle ORM [6] (drizzle-orm, drizzle-kit). Schéma et migrations dans drizzle/ ; commandes db:push, db:generate, db:migrate, db:studio en dev. Connexion en pooling (DATABASE_POOLING_URL_IP4 ou équivalent) ; toutes les écritures passent par un transaction helper partagé (withTransaction) avec rollback en cas d’erreur et logging structuré.
Auth et stockage : Supabase [7] (clients et utilitaires serveur compatibles SSR) pour l’auth par code à 6 chiffres envoyé par e-mail (OTP) et les fonctionnalités Supabase. La session s’appuie sur les cookies et mécanismes de session Supabase (HttpOnly, Secure, SameSite selon configuration).
Contenu : Content Collections [8] (@content-collections/core, @content-collections/markdown, @content-collections/next) pour le blog. Le Markdown vit dans content/blog/ avec un schéma Zod [9] (title en/fr, excerpt, slug, category, accessLevel, etc.). Nous utilisons remark-gfm [10] pour le Markdown style GitHub. Compilation au build uniquement ; pas de parsing Markdown côté client à l’exécution.
Routes API (Next.js App Router) :
POST /api/chat — chat streaming avec l’IA (Wagmi) ; limites de session, rate limiting, modération du contenu. Le contact avec l’équipe se fait uniquement via le chat — pas de formulaire de contact séparé.GET/POST /api/chat/status — statut / disponibilité du chat.GET /api/llm/status — statut du fournisseur LLM.GET /api/health — health check ; payload minimal en production (status uniquement), plus riche en dev (uptime, mémoire, env).POST /api/contacts/classification/request — classification de contact (utilisée par Wagmi pour les rôles).GET /api/auth/callback — callback d’auth Supabase.IA / LLM : Vercel AI SDK [11] (ai, @ai-sdk/openai, @ai-sdk/openai-compatible, @ai-sdk/react) avec Assistant UI [12] (et react-ai-sdk, react-markdown) pour l’UI du chat. La config LLM est validée avec Zod (LLM_API_URL, LLM_MODEL, LLM_INTERFACE, LLM_API_KEY) ; la production exige LLM_API_URL. Nous supportons les endpoints compatibles OpenAI (ex. Ollama en local).
Architecture LLM bi-niveau : Le chat opère sur deux niveaux de modèle. Les visiteurs anonymes sont servis par un petit modèle (Qwen 2.5 1.5B sur CPU via Koyeb) — rapide, économique, mais limité en raisonnement. Les utilisateurs authentifiés accèdent à un modèle plus gros sur GPU avec une meilleure gestion du contexte. Le routage de repli (GPU → CPU) garantit la disponibilité même quand le backend GPU est indisponible. La sélection de modèle est transparente : les visiteurs voient un avis que Wagmi est en « mode petit modèle » et sont incités à s’authentifier.
RAG local pour l’ancrage du petit modèle : Un modèle de 1,5B hallucine facilement. Nous avons donc construit un RAG léger de type BM25 (local-rag.ts). À chaque requête, le message de l’utilisateur est tokenisé (avec filtrage des mots vides EN/FR), scoré contre des segments pré-découpés de wagmi-skills.md et ai.txt, et les 4 meilleurs extraits sont injectés dans le prompt système. Pas de base vectorielle, pas d’embeddings — juste un scoring par chevauchement de tokens. Suffisant pour ancrer les réponses sur des faits vérifiés et empêcher le petit modèle d’inventer des services, des personnes ou des partenaires qui n’existent pas.
Dataset SFT pour le fine-tuning : Nous générons un dataset de Supervised Fine-Tuning (scripts/generate-wagmi-sft-dataset.ts) à partir des articles de blog, de la base de connaissances et de ai.txt. Le script produit 267 exemples d’entraînement et 47 d’évaluation au format JSONL, couvrant les garde-fous d’identité, les descriptions de services, l’incitation à l’authentification, l’expression de l’incertitude et les exigences de concision — le tout en français et en anglais. Le dataset est conçu pour des frameworks comme Unsloth et cible spécifiquement le modèle Qwen 1.5B.
Benchmark comportemental : Un script de benchmark dédié (scripts/benchmark-rag-qwen15b.ts) exécute plus de 20 cas de test contre le petit modèle avec et sans contexte RAG, mesurant la précision factuelle, le taux d’hallucination, la conformité d’incitation à l’authentification et la latence. C’est le garde-fou qualité du petit modèle : s’il échoue au benchmark, il n’est pas déployé.
Plafonnement des maxTokens : Les petits LLM (1,5B–3B) échouent souvent à émettre un token de fin de séquence, produisant du contenu correct suivi d’une répétition infinie. Nous plafonnons maxTokens à 300 pour les petits modèles (~200 mots, en accord avec les consignes du prompt) et 1024 pour les modèles plus grands.
Validation : Zod partout — corps des requêtes, variables d’env, schéma content-collections. Une entrée invalide échoue rapidement avec des réponses d’erreur typées.
Erreurs : Hiérarchie ApiError personnalisée (api-error.ts) : ValidationError, NotFoundError, etc., avec toJSON() pour des réponses API homogènes et intégration à un logger structuré (request id, session id, module). Aucune stack trace brute envoyée au client en production.
UI : React 19 [13] avec Tailwind CSS [14] et les primitives Radix UI [15] (Avatar, Dialog, Label, Slot, Tooltip). class-variance-authority et tailwind-merge pour les variantes de composants. Icônes : lucide-react (tree-shaking via optimizePackageImports de Next.js). Zustand pour l’état client quand nécessaire.
i18n : next-intl [16] (v4) pour EN/FR : messages dans src/i18n/messages/{en,fr}.json, locale dans le path ([locale]), métadonnées et alternates partagés pour le SEO.
Routing : App Router avec [locale] et route groups (routes) pour le blog et les pages, (legal) pour les pages légales. URLs canoniques et hreflang sont générés dans les métadonnées (voir metadata/utils.ts) pour que chaque page ait un bon <link rel="canonical"> et des alternates.
Performance (front) :
dynamic(..., { ssr: false }) pour garder le bundle principal léger ; webpack splitChunks sépare framework, lib (Radix, assistant-ui, lucide), vendor et common.deviceSizes/imageSizes réglés, CSP pour les images ; en export statique nous utilisons unoptimized quand il le faut.adjustFontFallback pour limiter le CLS ; display: swap et preload quand pertinent.SEO et crawlers : robots.txt, llms.txt, ai.txt pour les crawlers IA ; Schema.org JSON-LD généré côté serveur ; sitemaps pour le site et le blog. Pages auth/erreur en noindex.
Lint et format : Biome [18] (v2). biome check et biome format ; la config impose guillemets doubles, 120 caractères par ligne, LF et imports organisés. Pas d’ESLint/Prettier sur ce projet.
Git hooks : simple-git-hooks + lint-staged. En pre-commit : Biome sur *.{js,jsx,ts,tsx} et actionlint sur .github/workflows/*.{yml,yaml}. Aucun commit non formaté ou qui casse le lint.
Tests :
src/__tests__/unit, src/__tests__/integration, src/__tests__/security, src/components/__tests__. Couverture avec @vitest/coverage-v8 ; en CI nous lançons test:run et test:ci avec couverture.e2e/ (chat, hydration, error boundaries, reconnaissance de contacts). Pas de tests instables ; les tests font partie de la définition de fini.@lhci/cli) : lighthouserc.js définit les assertions (FCP, LCP, TBT, CLS, Speed Index, accessibilité, bonnes pratiques, SEO). Exécution sur les PRs vers main/dev et à la demande.Type checking : tsc --noEmit en étape dédiée (type-check). La CI le lance pour que la sécurité des types soit respectée avant merge.
Dépendances : Dependabot activé (npm hebdo, GitHub Actions [22] mensuel) avec regroupement des mises à jour mineures et patch. npm audit --audit-level=critical en CI avant build. Les overrides pour les problèmes connus (glob, rimraf, tar, cross-spawn) sont déclarés dans package.json.
GitHub Actions :
dev (et workflow_dispatch) : install des deps, audit critique, lint, test:run, puis build Docker [24] et push vers Docker Hub (jeanbapt/deal-ex-machina-web). Deuxième job : CLI Koyeb pour mettre à jour le service deal-ex-machina-staging/web (image Docker, env, health check sur /api/health, délai de grâce 60 s). Nous attendons le statut HEALTHY puis un dernier health check sur l’URL de staging.npm ci, build (avec env de placeholder), puis npm run lighthouse ; résultats déposés en artifacts (et optionnellement sur le serveur LHCI).workflow_dispatch) : health check staging optionnel, puis build avec NEXT_OUTPUT=export et CLOUDFLARE_PAGES=1, déploiement de out/ via Wrangler sur Cloudflare Pages. Environnements production et preview ; domaine personnalisé et redirects documentés.Docker : Dockerfile multi-stage (base Debian Bookworm slim, mises à jour de sécurité, utilisateur non-root nextjs). Copie uniquement de .next/standalone, .next/static et public ; démarrage avec node --max-old-space-size=512 server.js sur le port 8000. Pas de HEALTHCHECK dans l’image pour laisser Koyeb gérer les health checks. .dockerignore limite le contexte de build et exclut les artefacts de dev et la sortie Lighthouse.
Secrets : Aucun secret dans le dépôt. Nous utilisons les secrets GitHub Actions (ex. DOCKER_HUB_TOKEN, KOYEB_API_TOKEN, CLOUDFLARE_API_TOKEN_PAGE, CLOUDFLARE_ACCOUNT_ID) et les variables d’environnement au runtime (env Koyeb pour le service). La doc (ex. KOYEB_DOCKER_HUB_SECRET) décrit la configuration du registry et du déploiement.
On pourrait objecter qu'un seul hébergeur suffirait. Mais cela reviendrait à confondre simplicité opérationnelle et résilience architecturale. Le choix d'un déploiement mixte -- Koyeb pour le staging et le runtime serveur, Cloudflare Pages pour la production statique -- procède d'une logique délibérée de réduction des dépendances stratégiques.
Cloudflare Pages excelle là où l'edge excelle : distribution mondiale, cache agressif, protection DDoS, latence minimale pour les assets statiques. Pour un site dont le rendu principal est un export statique, c'est le substrat idoine. Pour autant, Cloudflare impose un modèle d'exécution propre -- Workers, runtimes contraints, APIs spécifiques -- qui, adopté sans recul, crée une dépendance saillante. Le jour où les conditions changent (tarification, limites techniques, choix stratégique), le coût de sortie peut être considérable. Quiconque a migré un projet entièrement câblé sur une plateforme propriétaire connaît le prix de cette découverte tardive.
Koyeb occupe le rôle complémentaire : héberger le runtime Node.js complet, les routes API, les connexions base de données et le chat IA, dans un environnement serveur classique. Le staging tourne sur Koyeb en permanence ; la production y basculerait en quelques heures si nécessaire, puisque l'image Docker est strictement la même. Koyeb apporte la souplesse d'un PaaS moderne (déploiement par image, scaling, health checks natifs) sans imposer un runtime propriétaire ni un format d'exécution verrouillé.
Ce faisant, nous obtenons la performance edge de Cloudflare pour le contenu statique, la souplesse d'un runtime Node complet pour les APIs et l'IA, et surtout la capacité de migrer sans réécriture. Le staging valide en continu que le mode serveur fonctionne ; la production prouve que l'export statique tient ses promesses de performance. Si l'un des deux hébergeurs devenait inadapté, le basculement relèverait de la reconfiguration, pas de la refonte.
La portabilité n'est pas un luxe théorique : c'est le prix d'entrée d'une architecture qui dure.
Le choix d'être Docker first sur Koyeb relève de la même discipline. Docker constitue l'abstraction la plus portable qui existe pour le déploiement d'applications : un Dockerfile, une image, un registre, et le runtime tourne indifféremment sur Koyeb, AWS ECS, Google Cloud Run, Fly.io ou n'importe quel VPS avec le démon Docker installé. Le jour où il faut migrer, on ne réécrit pas le déploiement -- on pointe l'image ailleurs.
A rebours de l'approche "tout-plateforme" (buildpacks propriétaires, runtimes managés, configurations spécifiques au fournisseur), le Dockerfile multi-stage décrit plus haut produit une image Debian slim, avec un utilisateur non-root, des mises à jour de sécurité et un binaire server.js autonome. Aucune dépendance sur les spécificités de Koyeb dans l'image elle-même. Le couplage avec la plateforme se limite à l'orchestration -- health checks, scaling, variables d'environnement -- soit le minimum incompressible que toute plateforme exige, et le seul qu'on accepte.
Cette discipline produit un bénéfice collatéral probant : la reproductibilité. L'image qui tourne en staging est identique à celle qui tournerait en production serveur. Le docker build local produit le même artefact que la CI. Il n'y a pas de "ça marche sur ma machine" parce que la machine est le conteneur. Pour un cabinet qui construit des systèmes pour d'autres, démontrer cette rigueur dans son propre déploiement relève davantage du devoir que de l'élégance.
Headers (production uniquement, dans next.config.mjs) : Content-Security-Policy (default-src 'self', script/style/img/font/connect adaptés, frame-ancestors 'none', object-src 'none', upgrade-insecure-requests), Strict-Transport-Security (max-age=31536000 ; includeSubDomains ; preload), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera, microphone, geolocation désactivés). poweredByHeader: false.
CORS : Origine autorisée configurable ; validation de l’Origin pour les requêtes qui modifient l’état ; méthodes et headers documentés. Les tests de sécurité couvrent CSRF/CORS (liste d’origines, exigences SameSite pour les cookies, POST uniquement pour les mutations).
Rate limiting : Limiteur en mémoire (par identifiant) : chat 20 req/15 min. Appliqué aux APIs publiques. (Le passage à Redis ou aux rate limits de la plateforme est documenté comme étape future.)
Entrées : Zod pour toutes les entrées API ; @2toad/profanity pour la modération du contenu dans le chat. Le SQL ne passe que par Drizzle (requêtes paramétrées). L’échappement React et la CSP limitent le XSS. Nous avons des tests dédiés pour CSRF, CORS, session fixation et validation des requêtes.
Health endpoint : En production la réponse health est uniquement { "status": "ok" } — pas de stack traces ni de détails internes.
Nous traitons la confidentialité et l’alignement réglementaire comme non négociables. Le site est conçu pour être préservant la vie privée par défaut et conforme au RGPD et à la loi européenne sur l’IA (règlement 2024/1689).
Minimisation des données et consentement : Nous ne collectons que le nécessaire. Chat : les conversations anonymes ne sont pas persistées ; nous enregistrons les messages uniquement lorsque l’utilisateur fournit un email et a donné un consentement explicite (case à cocher, enregistrée avec la soumission). Les données de chat stockées sont conservées 7 jours puis supprimées. Les données techniques (ex. IP, navigateur) sont limitées à ce qui est nécessaire au fonctionnement. Tout traitement est justifié au titre de l’article 6 du RGPD (consentement ou intérêt légitime). Le flux email du chat exige une case « J’accepte le traitement des données » et un lien vers la politique de confidentialité ; l’API accepte un flag consent et rejette ou ne persiste pas lorsqu’il est faux.
Droits et transparence : La politique de confidentialité (EN/FR) décrit ce que nous collectons, pourquoi et pendant combien de temps. Les utilisateurs sont informés de leurs droits (accès, rectification, effacement, limitation, portabilité, opposition) et peuvent contacter le contact désigné (type DPO) pour les exercer. Nous répondons dans les délais prévus par le RGPD. Cookies : nous n’utilisons que les cookies essentiels (ex. session, auth) ; pas de cookies de suivi ni d’outils d’analyse tiers. Les préférences cookies et une courte explication sont exposées dans l’interface.
Loi européenne sur l’IA : Nous nous alignons sur les obligations de transparence et de risque. Les utilisateurs sont clairement informés lorsqu’ils interagissent avec un système d’IA (le chat est présenté comme un assistant). Nous maintenons (ou pouvons produire) une documentation technique et un contrôle humain ; nous n’utilisons pas l’IA pour les pratiques interdites (scoring social, techniques manipulatrices ou subliminales, reconnaissance des émotions dans des contextes sensibles). Le chatbot est traité comme un système à risque limité et respecte les exigences de transparence (information, pas d’anthropomorphisme trompeur). Le signalement d’incidents est possible via l’adresse de contact. Les CGU et la politique de confidentialité font référence à la loi sur l’IA et dégagent la responsabilité en cas de reliance sur un contenu généré par IA.
Mise en œuvre : Le consentement est requis avant toute persistance de données de chat identifiantes ; l’API chat et le front l’imposent. Les pages confidentialité et légales sont liées depuis le footer et les flux de consentement. Il ne s’agit pas d’un passage de conformité unique : toute nouvelle fonctionnalité qui touche aux données personnelles ou au comportement de l’IA est conçue avec le RGPD et la loi sur l’IA en tête dès le départ.
Nous utilisons Lighthouse comme garde-fou qualité. Objectifs et résultats actuels (builds production locaux et CI) :
Scores Lighthouse (typiques) :
Core Web Vitals :
Les améliorations qui nous ont menés là : ChatSection chargée en lazy (~100 Ko différés), données structurées côté serveur, inlining du critical CSS, preconnect/preload, optimisation des fontes et code splitting agressif. Le détail est dans docs/ (ex. PERFORMANCE_RESULTS_FINAL.md, LIGHTHOUSE_RESULTS_96_PERCENT.md).
Pas de constructeurs de sites low-code ou no-code ; pas de WordPress ni de CMS générique pour le site principal. Pas d’any arbitraire ; pas de désactivation du TypeScript strict. Pas de console en production (le compilateur la retire). Pas de secrets dans le dépôt ni dans les bundles client. Pas de health endpoint qui fuite des infos internes en production. Nous ne livrons pas sans lint, type-check et tests en CI.
La direction compte autant que l’état actuel. La feuille de route est explicite : faire évoluer ce site vers une expérience IA native, où l’IA n’est pas un widget greffé sur une page classique mais la manière principale dont le produit réfléchit, assiste et s’adapte. Cette évolution se fait de façon incrémentale — pas à pas — pour que chaque changement soit livrable, mesurable et réversible.
Ce que « IA native » signifie ici : Le site et ses flux sont conçus avec l’IA comme acteur de premier plan. Contenu, navigation, contact (via Wagmi, le chat — pas de formulaire séparé) et découverte sont pensés pour qu’un assistant puisse comprendre le contexte, agir sur l’intention de l’utilisateur et s’améliorer avec l’usage — sans remplacer le noyau stable existant. Aujourd’hui nous avons un chat avec des rôles et des limites de session ; demain nous ajoutons un contexte plus riche (page, locale, messages précédents) et l’usage d’outils ; plus tard des suggestions proactives, de la synthèse ou des parcours guidés. Chaque étape est une amélioration discrète, avec le même niveau d’exigence : typage, tests, sécurité et performance.
Étapes d’évolution concrètes (illustratives, non exhaustives) :
Aucune de ces étapes ne nécessite une réécriture. La stack actuelle — Vercel AI SDK, Assistant UI, APIs validées par Zod et séparation nette serveur/client — est conçue pour absorber ces évolutions. La feuille de route est une séquence de telles étapes : chacune mergée, déployée et validée avant la suivante. L’objectif est un site qui se vit comme IA native parce que chaque ajout est pensé pour cela, et non plaqué après coup.
Cette stack est choisie pour que le site lui-même démontre :
Le site est entièrement codé à la main avec Cursor [1] : chaque route, composant et config décrits ci-dessus a été écrit et affiné dans l’éditeur, avec l’assistance de l’IA pour la vélocité et la cohérence, sans renoncer au contrôle sur l’architecture, la sécurité ou la performance. Si vous évaluez la profondeur technique, le dépôt et le site en ligne sont les livrables à examiner.
Liens vers les composants clés de la stack (sites officiels ou GitHub) :
Résumé des technologies clés : Node 20, TypeScript 5.9 (strict), Next.js 16 (App Router), React 19, Tailwind CSS, Drizzle ORM, PostgreSQL, Supabase, Content Collections, Zod, Vercel AI SDK, Assistant UI, next-intl, Radix UI, Biome, Vitest, Playwright, Lighthouse CI, Docker, Koyeb (staging), Cloudflare Pages (production), GitHub Actions.