« Ça marche en dev. » Soit. Mais ça ne dit rien sur la prod.
C'est le fil rouge de tout ce document. La prod ne casse presque jamais pour les raisons qu'on teste — elle casse pour des raisons invisibles en local. Trois principes gouvernent la suite : (1) le vrai travail est de neutraliser ces divergences invisibles ; (2) on met en ligne à la main d'abord, on comprend chaque étape, puis on automatise ce qui est stable ; (3) on choisit souverain-first — sans dogmatisme.
◆ Le parti pris — souveraineté-first, pas souveraineté-only
Un parti pris qui traverse tout le doc : il influence l'hébergement, l'auth, l'IA et la conformité.
« First » : par défaut, on choisit la brique la plus souveraine possible — données en UE, briques open-source auto-hébergées, fournisseurs européens. C'est un choix technique et commercial : dans un SaaS B2B européen, « vos données ne quittent pas l'UE » se vend, et t'évite de dépendre du bon vouloir (et des conditions tarifaires) d'un géant américain.
« Pas only » : ce n'est pas de la pureté idéologique. Là où un outil non-UE est nettement supérieur et sans substitut crédible — typiquement le meilleur modèle d'IA généraliste — on l'utilise, mais on l'architecture comme remplaçable : données minimisées, résidence choisie quand l'option existe, et une alternative souveraine câblée comme citoyen de première classe pour ne jamais être piégé.
On ne choisit pas Mistral OU Claude, on câble les deux. La voix passe par un modèle européen (Voxtral / Mistral), la génération peut tourner sur Claude ou Mistral ou un consensus des deux — un simple changement de mode, pas une réécriture. Le jour où le contexte (client, réglementation, prix) impose le 100 % souverain, la bascule est déjà là.
| Brique | Choix souverain-first | Le « pas only » assumé |
|---|---|---|
| Serveur | VPS européen (Hetzner Helsinki) | — aucun compromis nécessaire |
| Base de données | Postgres auto-hébergé | — |
| Auth | Better Auth open-source auto-hébergé | — |
| Emails | Fournisseur EU, domaine vérifié | — |
| Analytics | PostHog / Plausible auto-hébergés | — |
| IA | Mistral (EU) câblé en 1re classe | Claude (US) là où il excelle, derrière un runtime interchangeable |
01 Les décisions de fond
Ces choix structurent tout le reste. Les prendre avant d'écrire une ligne de déploiement évite de tout refaire dans trois semaines.
1.1 Où héberger
Deux familles. Plateforme managée (Vercel, Railway, Render, Coolify) : tu pushes, ça se déploie tout seul, zéro admin système — mais coûts qui grimpent avec le trafic, moins de contrôle, souvent données hors UE. Serveur nu / VPS (Hetzner, OVH, Scaleway) : une machine à toi, ~5-20 €/mois, contrôle total, données en UE — en échange c'est toi qui l'installes (mais une fois posée, ça bouge peu).
Le vrai critère n'est pas « lequel est le mieux » mais « combien de SaaS vais-je faire tourner ? ». Un seul produit qui démarre → un PaaS peut suffire. Dès deux produits → un VPS mutualisé devient nettement moins cher. Et côté souveraineté (§◆), un hébergeur européen ne coûte pas plus cher : c'est du souverain gratuit.
Un seul serveur Hetzner (~15 €/mois, Helsinki → données en UE, hébergeur européen) héberge tous les riffs du studio. Chaque nouveau SaaS = un port de plus, pas une machine de plus. Coût marginal d'un nouveau produit : ≈ 0 €. Déploiement natif (le programme tourne directement sur la machine via systemd), pas de couche d'orchestration.
On a d'abord été tenté par Coolify. Écarté : une surcouche de plus à déboguer pour zéro bénéfice à cette échelle. Build natif : 26 s. En Docker : 4 min. Le natif a gagné.
1.2 La base de données
C'est là que vivent tes clients. Trois règles non négociables : PostgreSQL par défaut (robuste, standard, gratuit) ; une base par produit, jamais de tables mélangées entre deux SaaS ; et les migrations sont du code versionné — le schéma évolue par fichiers commités dans git, jamais par des modifications manuelles en prod (intraçable, irréproductible).
Un serveur Postgres partagé, une base + un utilisateur dédiés par riff. Schéma géré par Prisma : prisma migrate deploy applique en prod exactement les migrations testées en local. On sortira une base dédiée à un produit quand il dépassera ~50 clients actifs — pas avant.
1.3 L'authentification
Ne jamais coder son propre système de mots de passe. Sessions, cookies, reset, OAuth : un nid à failles. Soit un service managé (Clerk, Auth0 — rapide mais payant au MAU et données chez un tiers), soit une librairie open-source auto-hébergée (Better Auth, Lucia — gratuit, données chez toi, souverain).
Better Auth auto-hébergé, sessions dans notre propre Postgres. Magic links + OAuth Google. Coût : 0 €, données chez nous.
Local en http://localhost, prod en https://. Les cookies de session « sécurisés » n'existent qu'en HTTPS — et leur nom même change entre les deux environnements. Un code qui cherchait le cookie par son nom en dur cassait toute la redirection en prod. Aucun test local ne le voit.
1.4 Le paiement
Stripe, quasi sans débat. Le point qui piège tout le monde : les webhooks. Stripe rappelle ton serveur pour dire « cet abonnement est actif / a échoué / annulé ». Mal branchés ou non vérifiés par signature → des clients qui paient sans être débloqués, ou l'inverse. À cadrer dès le départ : produits/prix créés par script idempotent (pas à la main), URL de webhook enregistrée + secret stocké, et un bypass de test pour dev/staging — strictement désactivé en prod.
Stripe via @better-auth/stripe (billing lié aux comptes). Produits/prix créés par script. Flag DEV_BYPASS_PAYWALL pour tester en staging, forcé off en prod.
Une mise à jour du plugin Stripe a ajouté silencieusement un champ que la base ne connaissait pas → webhook crashé, client bloqué après paiement. Invisible en dev (pas de vrais webhooks live). Leçon : surveiller les changelogs des libs qui écrivent en base, et monitorer les erreurs de webhook en prod.
1.5 Les emails transactionnels
« Vérifiez votre email », magic links, reçus : ça doit arriver, pas finir en spam. Jamais de SMTP perso ni de Gmail. Un service dédié (Resend, Postmark) + un domaine vérifié (SPF, DKIM, DMARC dans le DNS). Sans ça, la moitié de tes emails de bienvenue n'arrivent jamais et tu perds des inscrits sans le savoir.
Resend (free tier < 3000 emails/mois), domaine vérifié. En dev, clé absente → le lien s'affiche dans les logs. En prod, l'appli refuse de démarrer si la clé manque — sinon les liens de connexion partiraient en clair dans les logs sans que personne ne reçoive rien.
1.6 L'IA / le LLM — souveraineté-first en action
Un appel à un modèle d'IA n'est pas un appel normal : lent (secondes à dizaines de secondes), payant au token, faillible (format inattendu possible), non déterministe. Quatre règles :
- Timeout explicite sur chaque appel. 2 s en local, 40 s ou timeout en prod sous charge. Sans timeout, une requête bloquée fige tout.
- Ne jamais faire confiance au format de sortie. Même avec un schéma imposé, le modèle peut le déborder. Valider et re-couper côté serveur.
- Streaming pour les réponses longues, plutôt qu'un écran blanc.
- Suivre le coût par appel et par mois dès le départ.
C'est aussi l'endroit où la souveraineté se joue vraiment (§◆). Le LLM est là où la tentation du 100 % américain est la plus forte. Le parti pris n'est pas de s'en priver, mais de ne pas s'y enchaîner : câbler un modèle européen comme runtime de première classe, derrière la même interface, pour faire tourner l'un, l'autre, ou les deux en consensus — sans réécrire une ligne de produit.
Runtime IA dual derrière une seule interface (extractWithMode.ts) : chaque extraction tourne en mode claude, mistral, ou consensus (les deux, on croise). La transcription vocale passe par Voxtral (Mistral, EU) — la donnée voix, la plus sensible, ne sort jamais de l'espace européen. La génération de fond utilise Claude là où sa qualité fait la différence, mais le mode est une variable, pas du code en dur : basculer une orga, un client ou tout le produit vers le 100 % Mistral = changer un mode.
Le « pas only » en clair : Claude reste utilisé, assumé, parce qu'il est meilleur sur la génération complexe aujourd'hui. Mais l'architecture garantit qu'aucun client, aucune réglementation, aucune hausse de prix ne peut nous piéger : l'alternative souveraine est déjà branchée et testée, pas un chantier futur.
Les modèles (Claude comme Mistral) traitent les contraintes de longueur comme des suggestions, pas des règles. Un texte trop long remontait jusqu'à la validation stricte du serveur, rejeté en erreur 400 → toute l'opération plantait. Fix : couper la longueur côté serveur, et ne jamais faire échouer l'opération principale pour une donnée cosmétique.
02 La parité dev / prod
La section à faire lire à ton dev (ou à toi, si tu codes avec Claude). C'est ici que 80 % du travail sérieux se joue.
La quasi-totalité des bugs livrés en prod passent les tests locaux et cassent quand même — parce qu'ils naissent d'une divergence silencieuse entre l'environnement local et celui de prod. Les diffs inévitables (adresse du site, mode de build) sont OK ; ce sont les diffs invisibles qui tuent.
| # | Le piège | Ce qui le déclenche | La parade |
|---|---|---|---|
| 1 | Variable de config oubliée | En dev, clé absente = « plan B » silencieux. En prod, la feature no-op sans erreur visible. | Validation au démarrage : l'appli refuse de booter en prod si une clé critique manque. |
| 2 | URL en dur (localhost) | Un email prod contient un lien vers localhost:3000. Callback mal pointé. | Toute URL absolue se lit depuis une variable d'environnement, jamais en dur. |
| 3 | Cookies & HTTPS | Local HTTP, prod HTTPS → cookies refusés, nom du cookie qui change. | Forcer HTTPS en prod, ne jamais coder le nom d'un cookie en dur. |
| 4 | Latence réseau réelle | Appel externe à 30 ms en local, timeout en prod sous charge. | Timeout + retry explicites sur tout appel externe. |
| 5 | Build prod ≠ dev | Le mode dev est tolérant ; le build optimisé élague du code et casse au premier hit. | Lancer le vrai build de prod en local avant chaque déploiement. |
| 6 | Vraies données ≠ données de test | Un filtre marche sur des données bidon, renvoie 0 sur les vraies. | Tester avec une copie des vraies données pour tout ce qui touche filtrage/droits. |
| 7 | Fichiers fantômes | Un fichier renommé en local reste sur le serveur et est servi à la place du bon. | Synchroniser avec suppression des fichiers obsolètes (rsync --delete). |
| 8 | Cache serveur périmé | Le serveur sert l'ancien contenu après une modif. | Vider le cache de build avant chaque build de prod. |
Un fichier env-validation.ts lancé au démarrage liste les variables critiques et leurs règles (« requis + HTTPS », « requis si Stripe activé »…). En prod, boot refusé si une règle est violée ; en dev, no-op. Chaque règle est couverte par un test. À côté : checklist pré-déploiement (build local, recherche de localhost en dur, test avec vraies données) et checklist post-déploiement automatique.
Demande à ton dev : « comment l'appli se comporte-t-elle si telle clé manque en prod ? ». Si la réponse est « elle plante silencieusement » ou « je ne sais pas », il manque la validation au boot.
03 Le chemin de mise en ligne
La séquence réelle du premier déploiement. À faire une fois à la main pour tout comprendre, puis à automatiser.
- Domaine + DNS. Acheter le domaine, pointer l'enregistrement
Avers l'IP du serveur. (Piège : lewwwn'est pas créé automatiquement — l'ajouter à la main.) - Accès serveur. SSH par clé uniquement (jamais par mot de passe), firewall fermé par défaut.
- Base de données. Créer la base + l'utilisateur dédiés, noter l'URL de connexion.
- Fichier de secrets de prod. Un
.env.productionsur le serveur, permissions restreintes (chmod 600), toutes les vraies clés. Jamais dans git. - Code → install → migration → build. Transférer le code, installer avec un lockfile figé (versions exactes), appliquer les migrations, builder pour la prod.
- Faire tourner en service. Le déclarer en service système (
systemd) pour qu'il redémarre tout seul après un crash ou un reboot. - Reverse proxy (Nginx). La porte d'entrée : reçoit le HTTPS public, le passe au programme en interne. Gère aussi rate-limiting et taille max des uploads.
- Certificat HTTPS. Let's Encrypt (gratuit), renouvellement automatique. (Piège : la mauvaise méthode coupe le serveur web et fait tomber tes autres sites — utiliser « webroot ».)
- Vérifier depuis 3 angles. Santé interne, santé via le proxy, santé publique en HTTPS. Un endpoint
/api/healthqui répondokest le minimum vital.
Tout est scripté dans deploy-dwg.sh (synchronise, installe, migre, build, redémarre, vérifie la santé — et échoue bruyamment si le health check ne répond pas). Premier déploiement fait pas à pas à la main, puis figé dans le script.
Le programme qui écoute sur la mauvaise interface réseau (504 depuis le proxy) ; les fichiers statiques que le build de prod ne copie pas tout seul → page sans style ; l'outil de migration qui cherche .env et pas .env.production. Chacun a coûté une heure la première fois, zéro depuis qu'ils sont dans le script.
04 Ce qui doit tourner tout seul
La prod n'est pas « mise en ligne puis oubliée ». Quatre automatismes, par priorité — à ne monter qu'une fois le déploiement manuel maîtrisé.
4.1 Déploiement automatique (CI/CD)
Quand le code est stable : un push sur la branche principale déclenche synchro + déploiement + test de fumée (la page d'accueil et la connexion répondent) + notification.
GitHub Actions : push sur main → déploiement + smoke test + notif. Chaque PR déploie une copie de staging (non indexable, données séparées). Rollback = relancer le workflow sur un ancien commit.
4.2 Surveillance (monitoring)
- Logs consultables.
- Capture d'erreurs (Sentry) : t'alerte quand un utilisateur rencontre une erreur, avec la trace. Free tier suffisant.
- Uptime externe : ping
/api/healthchaque minute, alerte si le site tombe. - Analytics produit (PostHog, Plausible — auto-hébergés, souverains) pour comprendre l'usage.
Sentry (erreurs), PostHog auto-hébergé (analytics + session replays masqués + opt-out par client), ping externe sur /api/health. Un cron compare même le code déployé au dernier commit et redéploie tout seul en cas d'écart.
4.3 Sauvegardes (backups)
La question qui tranche : « si le serveur brûle maintenant, qu'est-ce que je perds ? » La réponse doit être « quelques heures de données, pas tout ».
- Base de données : dump quotidien, rétention 30 j, copie hors-site (un backup sur la même machine ne protège de rien).
- Code : git/GitHub est déjà ta source de vérité.
- Secrets : copie chiffrée hors serveur.
- Tester la restauration : un backup jamais restauré n'est pas un backup.
Dump Postgres quotidien (3 h, rétention 30 j) + copie chiffrée hors-site (restic vers un stockage distinct). Secrets dans KeePassXC. Procédure de restauration documentée et éprouvée.
4.4 Tâches planifiées (cron)
Relances, rapports hebdo, agrégations : des appels déclenchés par l'horloge. Les sécuriser avec un secret pour que seul le cron puisse les déclencher, pas n'importe qui sur internet.
05 Sécurité & conformité
Le minimum sérieux pour un SaaS UE qui manipule des données clients.
- Secrets. Jamais dans git. Fichier de prod en
chmod 600. Copie chiffrée hors ligne. Préparer le geste de révocation si un secret fuite. - Accès serveur. SSH par clé, firewall fermé par défaut, seuls les ports nécessaires ouverts.
- Isolation multi-clients. Aucune requête ne doit pouvoir lire les données d'un autre client. Centraliser ce filtre — un oubli = une fuite.
- RGPD. Données en UE (argument commercial en plus), suppression réelle sur demande, politique de confidentialité claire. Session replay → masquer les champs sensibles + opt-out.
- Rate limiting au niveau du proxy pour absorber les abus.
- Dépendances. Scan de vulnérabilités en intégration continue, avec un seuil qui bloque en cas de faille grave.
Isolation multi-tenant via un wrapper centralisé (jamais de requête sans filtre client). Hard delete RGPD. Session replay masqué + opt-out par organisation. pnpm audit en CI avec seuil bloquant. Serveur EU, briques auto-hébergées, voix traitée par un modèle européen (Voxtral), option IA 100 % souveraine (Mistral) activable par simple changement de mode. « Vos données restent en UE » n'est pas un slogan — c'est vérifiable ligne à ligne, et c'est un argument de vente assumé.
06 Les coûts
Ordre de grandeur pour un SaaS solo. Le seul poste qui scale avec l'usage, c'est l'IA.
| Poste | Fourchette | Note |
|---|---|---|
| Serveur (VPS) | 5-20 €/mois | Mutualisable entre plusieurs SaaS |
| Domaine | ~1 €/mois amorti | |
| Emails | 0 € au départ | Free tier < ~3000/mois |
| Auth | 0 € | Si auto-hébergé (sinon au MAU) |
| Base de données | 0 € | Incluse dans le VPS |
| Capture d'erreurs | 0 € | Free tier |
| Backups | ~0-5 €/mois | Stockage hors-site |
| IA / LLM | variable | Le seul poste qui scale avec l'usage. Voxtral (EU) ~0,0015 €/transcription — négligeable |
| Paiement (Stripe) | ~1,5 % + 0,25 € | Prélevé sur le CA, pas un coût fixe |
~80-300 €/mois selon l'usage IA, l'essentiel étant l'IA. À 50 clients sur une offre à ~99 €/mois → ~5000 € de revenu mensuel récurrent, marge très confortable. Serveur, emails, auth, backups : tous mutualisés, coût marginal proche de zéro.
07 Checklist « go-live »
À cocher avant d'ouvrir au public.
Fondations
- Domaine acheté, DNS
A(+www) pointés sur le serveur - Base de prod créée, migrations appliquées
- Fichier de secrets en place,
chmod 600, hors git - HTTPS actif, renouvellement du certificat automatique
Parité dev / prod
- Validation des variables au démarrage (boot refusé si clé critique manquante)
- Aucune URL
localhosten dur - Le vrai build de prod tourne en local sans erreur
- Cookies / session OK en HTTPS
- Timeout + retry sur chaque appel externe
Paiement (si applicable)
- Produits / prix créés par script
- Webhook enregistré et vérifié par signature
- Bypass de test désactivé en prod
- Un vrai cycle de paiement testé de bout en bout
Filet de sécurité
- Endpoint de santé (
/api/health) qui répond - Capture d'erreurs branchée
- Uptime externe qui ping le health check
- Backup base quotidien hors-site, restauration testée
- Secrets copiés dans un coffre chiffré
Souveraineté & conformité
- Hébergement & données en UE
- Alternative IA souveraine câblée (pas seulement un fournisseur US)
- Isolation multi-clients centralisée et vérifiée
- Suppression de données RGPD possible
- Politique de confidentialité en ligne
- SSH par clé, firewall fermé par défaut
08 Modèle de PRD à remplir
À copier pour chaque nouveau SaaS. Une page, remplie en 30 min, qui fige les décisions avant de coder le déploiement.
# PRD mise en prod — <NOM DU SAAS> ## Produit - Pitch en une phrase : - Cible : - Scope V1 (ce qui doit marcher au lancement) : - HORS V1 (explicitement) : ## Décisions de fond (souveraineté-first, pas -only) - Hébergement : # PaaS / VPS — lequel, UE ? - Base de données : # Postgres partagé / dédié - Auth : # managé / auto-hébergé — lequel - Paiement : # Stripe ? offres, prix, script setup ? - Emails : # fournisseur EU + domaine vérifié - IA / LLM : # runtime souverain (Mistral) + US (Claude) ? # mode interchangeable ? voix en EU ? ## Domaine & infra - Domaine : - IP serveur / port : - Base + user : - Secrets de prod : chemin + copie chiffrée ## Variables critiques au boot | Variable | Règle prod | Pourquoi | |----------|------------|----------| | | | | ## Automatismes - CI/CD : # oui/non, quand - Monitoring : # erreurs / uptime / analytics - Backups : # fréquence, rétention, hors-site - Crons : # quels jobs, quelle fréquence ## Coûts / mois - Fixe : - Variable (IA) : - Seuil de rentabilité (nb clients × prix) : ## Go-live - Date cible : - Checklist §7 cochée : oui / non
Ce PRD et les checklists sont prêts à copier — versions Markdown dans le repo, avec un CLAUDE.md à déposer dans ton projet pour que ton IA les déroule avec toi :
→ github.com/garniera93/saas-souverain-methode