N° 07 ·

Comment j'ai mis mon SaaS en ligne, de A à Z

Schéma de l'architecture de Nadia : le navigateur et l'agent entrent par Traefik, une image Docker porte le frontend, le backend et le worker, dessous Postgres, Redis et Temporal, à côté R2.

Salut, c’est Vincent.

Si tu viens d’arriver : je fais un challenge, une vidéo par jour pendant 100 jours, et aujourd’hui c’est le jour 85.

Ce mail est long, je te préviens. Pas de skill cette semaine : je t’ouvre le capot de mon SaaS.

Il s’appelle Nadia. C’est l’outil avec lequel je publie mes vidéos sur Instagram et TikTok, je réponds aux commentaires et je mesure ce qui marche. Chaque vidéo du challenge passe par lui. Et hier, pour mes 35 ans, il était prêt.

Où j’en suis, sans filtre : pré-lancement, 0 € de revenu récurrent. Le principal utilisateur, c’est moi. Quelques potes ont un accès, quelques clients en bêta vont sûrement payer, mais rien n’est signé.

Donc ce mail, ce n’est pas « comment j’ai réussi ». C’est comment on met un SaaS en ligne, de A à Z, et ce que ça coûte. Les liens sont à la fin.

Dis-moi ce que tu en penses en répondant à ce mail.


Ce que fait Nadia

Nadia, c’est une application web, usenadia.com. Tu connectes tes comptes Instagram et TikTok, et ensuite :

  • tu programmes tes posts dans un calendrier, et Nadia les publie à l’heure dite ;
  • tu lis tes commentaires et tes DM dans une boîte de réception, sans ouvrir les apps ;
  • tu poses une automatisation sur une publication : quelqu’un commente un mot, il reçoit un DM avec ton lien. Chaque « commente SUPABASE et je t’envoie le lien » du challenge passe par là ;
  • tu vois les chiffres de chaque vidéo, collectés tous les jours ;
  • et tout ça se pilote par un agent, j’y viens juste après.

Ce que fait Nadia : toi ou ton agent d’un côté, Instagram et TikTok de l’autre, et au milieu le calendrier, la boîte de réception, les automatisations et les stats.

Si tu connais le marché : c’est Metricool pour programmer et mesurer, plus ManyChat pour les DM automatiques, plus un agent qui pilote le tout, dans un seul outil et un seul abonnement, sans compteur de DM.

Le côté agent, c’est le cœur

Nadia est agent-first. Tout ce que l’API publique sait faire, un outil MCP le fait aussi, et un test en CI refuse la release si une route sort sans son outil. Concrètement : tu branches Nadia comme connecteur dans Claude, ou dans Claude Code en une ligne, et tu lui parles. « Programme la vidéo du jour sur Instagram et TikTok à 18 h. » « Qui a commenté SUPABASE hier ? » « Sors-moi les vues des sept dernières vidéos. » Il y a aussi un nœud n8n et une ligne de commande pour ceux qui préfèrent.

C’est comme ça que je l’utilise tous les jours : ma chaîne de montage dépose la vidéo, et c’est Claude Code qui programme le post, pose l’automatisation et relit les commentaires. Ce que tu peux faire en cliquant, ton agent peut le faire en parlant.

Nadia se pilote par un agent : Claude Code, Claude ou n8n parlent à Nadia par MCP et par l’API, 79 routes et 79 outils, et Nadia parle à Instagram, TikTok et à ton outil d’e-mail ; trois phrases d’exemple.

Comment je l’ai construit

Nadia est en ligne depuis la mi-juillet, sur un seul serveur. Le premier post est parti le 18 juillet, et il y en a eu 211 depuis. Il a grossi avec le challenge : chaque fois qu’il me manquait quelque chose pour publier, répondre ou mesurer, je l’ai ajouté. Depuis juillet, 601 commits et 60 pull requests, dont une bonne partie écrites par des sessions Claude Code dans le cloud pendant que je tournais. Dans l’ordre : le calendrier et la publication, la boîte de réception, les automatisations commentaire vers DM, les stats par vidéo, et le serveur MCP pour que Claude publie à ma place.

L’architecture, pièce par pièce

Cinq pièces, et chacune a une raison d’exister.

Le frontend, en Next.js. L’interface : le calendrier, le composeur, la boîte de réception. Il ne décide rien, il appelle l’API.

Le backend, en NestJS. L’API. Il reçoit « programme ce post mardi à 10 h », vérifie que tu as le droit, l’écrit en base et passe la main au worker. Il porte aussi l’API publique et le serveur MCP : 79 routes, chacune doublée d’un outil MCP.

Le chemin d’une demande : le navigateur, le frontend Next.js, l’API NestJS, Postgres, puis Temporal qui garde le post jusqu’à l’heure.

Le worker, avec Temporal. La pièce que la plupart des gens découvrent trop tard. Un post programmé pour mardi doit partir mardi, même si le serveur a redémarré lundi. Une vidéo envoyée à TikTok n’est pas publiée tout de suite, il faut lui redemander pendant vingt minutes. Et si Instagram répond une erreur, il faut réessayer, sans publier deux fois. Temporal fait exactement ça : chaque post est un workflow qui survit aux redémarrages, dort jusqu’à l’heure, et réessaie selon les règles que tu écris. Le même worker lance chaque nuit la collecte des stats, la synchro des boîtes de réception et le renouvellement des jetons.

La vie d’un post dans le worker : il dort jusqu’à l’heure, publie, attend la réponse de TikTok en redemandant toutes les dix minutes, réessaie trois fois en cas d’erreur, puis te notifie.

Postgres, la base : 86 tables. Redis, pour le rapide et le jetable : sessions, verrous, files. Et à côté, le stockage des vidéos, qui a failli tout casser, j’y reviens.

L’architecture de Nadia : le navigateur et l’agent entrent par Traefik ; une image Docker porte le frontend, le backend et le worker ; dessous Postgres, Redis et Temporal ; à côté R2 pour les vidéos ; le worker parle à Instagram et TikTok.

Les trois applications sont dans un seul monorepo et partent en production dans une seule image Docker. Moins propre qu’une image par service, et très bien pour un seul serveur.

Le serveur

Une seule machine chez Hetzner : 8 vCPU, 16 Go de RAM, 160 Go de disque, autour de 16 € par mois. Un pare-feu qui ne laisse passer que SSH, HTTP et HTTPS, et fail2ban pour ceux qui insistent sur le port 22.

Hier après-midi, huit conteneurs tournaient dessus : Nadia, Postgres, Redis, Temporal avec son Postgres et son Elasticsearch, Traefik qui fait entrer le trafic, et un petit agent de monitoring. Il reste de la place.

Le serveur de prod : Hetzner, 8 vCPU, 16 Go, 160 Go ; les huit conteneurs rangés en quatre groupes, app, infra, entrée et monitoring.

Il y a un deuxième serveur, « outils », qui n’héberge rien que tu verras. C’est là que vit Coolify.

Le déploiement

Coolify, c’est un panneau de contrôle open source qui fait ce que Heroku ou Vercel font pour toi : il lit un docker-compose dans ton repo, lance les conteneurs, pose les certificats HTTPS, et redéploie quand tu lui demandes.

Au début, il tournait sur le même serveur que Nadia. Je l’ai sorti début octobre : quatre conteneurs de moins sur la prod, et le panneau de contrôle de ma prod n’a aucune raison d’être exposé sur Internet. Aujourd’hui il tourne sur « outils », joignable seulement par Tailscale, un VPN privé entre mes machines, et il pilote la prod par le réseau privé de Hetzner.

Deux serveurs : « outils » porte Coolify, joignable seulement par Tailscale, et pilote « prod » par le réseau privé Hetzner ; Internet n’entre sur prod que par les ports 80 et 443.

Le chemin d’un déploiement :

  1. Je merge sur main. Il ne se passe rien, un push ne déploie pas.
  2. pnpm release pose un tag de version, du type v2026.10.26.
  3. Le tag réveille GitHub Actions, qui construit l’image Docker en neuf minutes et la pousse sur le registre de GitHub.
  4. Coolify fait tirer la nouvelle image par le serveur et redémarre Nadia. 34 secondes de 502, puis c’est en ligne.

Le chemin d’un déploiement : pnpm release pose un tag, GitHub Actions construit l’image en neuf minutes, la pousse sur le registre GitHub, Coolify la fait tirer par le serveur, 34 secondes de 502, en ligne.

Pourquoi construire chez GitHub et pas sur le serveur ? Parce que le build demande 4 Go de mémoire à lui seul, pendant neuf minutes, et que la prod n’a pas à ralentir à chaque release. Règle absolue : le serveur ne construit jamais, il tire une image toute faite.

Deux leçons que j’ai payées :

Les minutes GitHub Actions. En septembre, chaque push lançait le build complet, et chaque merge reconstruisait l’image. C’est le problème du mode agentique : des sessions Claude Code poussent des commits à la pelle, chaque commit réveille la CI, et la facture suit. Mesuré : 4 000 minutes par mois, au-dessus des 3 000 de mon abonnement. Depuis le 24 septembre, seul le tag déclenche. Dix merges entre deux releases, c’est un build au lieu de dix.

Le redéploiement qui coupait tout. Un seul docker-compose portait l’app et la base. Chaque déploiement redémarrait donc Postgres, Redis et Temporal avec l’app : trois à quatre minutes de panne, et les posts programmés pendant ce temps-là partaient en retard. Le 22 septembre, j’ai coupé en deux : une ressource « infra » que je ne redéploie jamais, et une ressource « app » avec la seule image Nadia. Mesuré le jour même : 34 secondes au lieu de trois minutes.

Avant : un seul docker-compose, l’app et la base redémarrent ensemble, trois à quatre minutes de panne et des posts en retard. Après : deux ressources, l’app seule redémarre, l’infra ne bouge pas, 34 secondes.

Les données, et ce qui a failli tout casser

Les vidéos. Au départ, elles étaient stockées sur le disque du serveur. En septembre, les uploads prenaient déjà 8 Go, et le déploiement a commencé à refuser de tirer l’image, faute de place. Un seul utilisateur, c’est à peu près 5 Go de vidéo par mois. Le 21 septembre, j’ai tout basculé sur Cloudflare R2, un stockage d’objets compatible S3 : le navigateur envoie les gros fichiers directement chez Cloudflare, sans passer par mon serveur. Et un workflow de nuit supprime les vidéos sept jours après leur publication, après en avoir gardé une copie 480p. Les plateformes ont la vidéo, je n’ai pas besoin de la garder en 4K.

La base. Le 27 septembre, pour l’audit TikTok, j’ai dû supprimer mon compte TikTok de Nadia et le reconnecter. Ce que je n’avais pas en tête, c’est que supprimer un canal efface tout ce qui va avec, pour de bon, comme la loi le demande : 1 809 exécutions d’automatisations sont parties avec. Au moins, je sais que ça marche 😂 Il n’y avait aucune sauvegarde. Le lendemain, j’ai posé un pg_dump tous les matins à 4 h, sept copies sur le serveur et une copie dans un bucket R2 privé. Restauration testée le jour même : deux secondes pour une base de 25 Mo.

Les vidéos : le navigateur envoie directement chez Cloudflare R2 ; sept jours après publication, une copie 480p part en archive et l’original est supprimé. La base : pg_dump à 4 h, sept copies sur le serveur, une copie dans un bucket privé qui expire après huit jours.

Le site marketing

Le site usenadia.com n’a rien à voir avec l’app : un site statique en Astro, zéro JavaScript côté visiteur, déployé sur Cloudflare Workers à chaque push qui le touche. Le français à la racine, l’anglais sous /en, la doc sous /docs, écrite à la main. Pourquoi le séparer de l’app ? Parce que le site doit tenir quand l’app redémarre, et qu’un site statique chez Cloudflare ne coûte rien et ne tombe pas.

Deux chemins depuis le même repo : un push qui touche le site part en statique sur Cloudflare Workers, usenadia.com ; un tag part en image Docker sur le serveur, app.usenadia.com.

Les erreurs que je ne referai pas

  • Des secrets dans le repo. Jusqu’au 22 octobre, le docker-compose portait les vraies valeurs des clés. Le repo est privé, mais c’est privé jusqu’au jour où ça ne l’est plus. Un audit de sécurité les a sortis, et j’ai tout fait tourner le soir même. Aujourd’hui le compose ne contient que des ${VAR}, les valeurs vivent dans Coolify.
  • Une migration de base dans le mauvais ordre. Hier, j’ai renommé les offres dans la base, et j’ai déployé l’image avant de passer le script SQL. L’app a redémarré en boucle pendant huit minutes. La règle est maintenant écrite dans ma skill de déploiement : le script SQL avant le tag, jamais après.

Où j’en suis, pour de vrai

Les prix sont posés depuis vendredi : Solo à 29 € par mois pour 5 comptes, Équipe à 49 € pour 15, Agence à 99 € pour 50. Le prix dépend du nombre de comptes connectés, et de rien d’autre : pas de compteur de DM, pas de compteur de contacts, l’agent et l’API sont dans toutes les offres. Ce sont des prix largement sous le marché, parce que je cherche mes premiers clients, pas ma marge. Et il y a un code de pré-lancement, valable une semaine, jusqu’au mardi 3 novembre, et qui te garde le prix à vie : Solo à 19 € au lieu de 29, Équipe à 29 € au lieu de 49. Je ne le mets pas dans ce mail : si ça t’intéresse, réponds-moi, je te le donne.

Le paiement passe par Stripe depuis hier, et l’inscription est ouverte : tu crées ton compte, tu choisis une offre, et les sept premiers jours sont à 0 €, carte demandée, résiliable avant le premier prélèvement. Une chose à savoir : sur TikTok, la publication publique attend encore un dernier audit de leur part. En attendant, un post TikTok programmé arrive en brouillon dans ton app TikTok, et tu le publies en un clic. Instagram, lui, est complet. Facebook, LinkedIn, X et Threads viendront ensuite.

Et un petit scoop, puisque tu es arrivé jusqu’ici. Mon prochain challenge, à la fin des 100 jours, ce sera de construire ce SaaS en public : trouver ses clients, le vendre, le montrer. Je continuerai à donner des exemples et des skills concrets de ce que je fais tous les jours, mais mes prochaines vidéos seront moitié là-dessus, moitié éducatives, sur Nadia. Tu le verras naître ici en premier.

Si tu veux voir à quoi ressemble un SaaS avant ses premiers clients, c’est maintenant. Et si tu le testes, dis-moi ce qui coince, c’est exactement ce dont j’ai besoin.

Découvrir Nadia

Les liens


Si tu es arrivé jusqu’ici, réponds-moi juste « lu », ça me dit si le format long vaut le coup. Et si tu as une question sur une pièce en particulier, le worker, Coolify, les backups, pose-la : les prochains numéros peuvent creuser une pièce à la fois.

À mardi prochain, Vincent

Un mail · le mardi · gratuit

Le prochain, dans ta boîte

Un mail par semaine, le mardi.

Désinscription en un clic sur chaque mail. Je hais le spam.