tech

9 SaaS dans un monorepo Next.js : notre stack

Comment OmniRealm construit 9 produits SaaS dans un monorepo PNPM. Architecture, packages partages, deploy unifie et lecons apprises.

6 min

Pourquoi un monorepo pour 9 produits SaaS

Quand nous avons lance OmniRealm, la question s'est posee immediatement : un repo par produit ou un monorepo pour tout l'ecosysteme ? La reponse est venue de l'arithmetique.

Avec 9 produits SaaS (Wardek, FiscalPilot, OmniBookmark, OmniTask, OmniScan, FitRealm, IrisPro, Arena, Mission Brevet), chaque produit partage des besoins communs : authentification, composants UI, gestion d'erreurs, logging, SEO, configuration d'environnement, deploy.

En multi-repo, chaque besoin commun signifie soit de la duplication (9 implementations differentes de la meme chose), soit un package NPM prive (avec son overhead de publication, versioning, CI). Les deux options ralentissent le developpement.

En monorepo, un changement dans le package d'authentification est immediatement disponible dans les 9 produits. Un nouveau composant UI est utilisable partout en une ligne d'import. Un fix de securite se propage instantanement.

La stack technique

PNPM Workspaces

Le monorepo utilise PNPM avec son systeme de workspaces. PNPM offre trois avantages critiques par rapport a npm ou yarn :

Installation rapide : Le content-addressable store de PNPM evite la duplication des packages sur le disque. Avec 9 apps et 48 packages internes, c'est une difference de plusieurs gigaoctets.

Isolation stricte : PNPM n'utilise pas le hoisting agressif de npm/yarn. Chaque package a acces uniquement aux dependances qu'il declare explicitement. Cela evite les phantom dependencies, un probleme courant en monorepo.

Workspace protocol : Le prefixe workspace:* permet de referencer les packages internes sans numero de version. La resolution est automatique et instantanee.

Next.js pour chaque app

Chaque produit SaaS est une application Next.js independante avec App Router. Le choix de Next.js est motive par plusieurs facteurs :

  • Server Components pour des performances optimales
  • SSG pour les pages marketing (TTFB < 200ms)
  • API Routes integrees pour les backends legers
  • Un ecosysteme middleware mature (auth, i18n, rate limiting)

Chaque app utilise une configuration Next.js standardisee qui inclut transpilePackages pour les packages internes et outputFileTracingRoot pour le build Docker.

TypeScript strict partout

Zero compromis sur les types. TypeScript est en mode strict dans tout le monorepo : strict: true, noUncheckedIndexedAccess: true, pas d'implicit any. Chaque package interne exporte ses types.

Cela represente un investissement initial plus lourd, mais les benefices sont immenses : les erreurs sont detectees au build, pas en production. La refactorisation est sure. L'auto-completion est exhaustive.

Les 48 packages partages

Le coeur du monorepo, c'est la couche de packages. 48 packages dans packages/@omnirealm/ qui couvrent tous les besoins transversaux.

Packages infrastructure

@omnirealm/logger : Logging structure base sur Pino. Server-side avec rotation, client-side avec fallback console. Interceptor pattern pour le logging automatique des services.

@omnirealm/config : Validation d'environnement avec Zod. Chaque app declare son schema d'env et obtient une validation au demarrage avec des messages d'erreur explicites.

@omnirealm/errors : Hierarchie d'erreurs typees (NotFoundError, ValidationError, AuthorizationError) avec serialisation et deserialization coherentes.

Packages metier

@omnirealm/auth : Authentification et autorisation partagees. Session management, JWT, roles et permissions via CASL.

@omnirealm/seo : Generation de metadata, sitemaps, JSON-LD, Open Graph. Chaque produit appelle une fonction et obtient un SEO complet.

@omnirealm/ui : Composants UI partages. Boutons, formulaires, modales, layouts. Base sur Tailwind avec des design tokens coherents.

@omnirealm/testing : Mocks centralises pour Prisma, Stripe, Resend, Anthropic. Utilities de test partagees. Un mock ecrit une fois, utilise dans 9 apps.

Le taux de reutilisation

Sur les 9 apps, 66% du code est partage via les packages internes. Cela signifie que quand nous creons une nouvelle app, les deux tiers du code sont deja ecrits. Le temps de creation d'un nouveau produit est passe de 3 semaines a 3 jours.

Le deploy unifie

Chaque app a son deploy.sh qui appelle un script commun deploy-unified.sh. Ce script gere :

  1. Build Docker avec le contexte monorepo
  2. Preparation des dependances vendored (pour les packages internes)
  3. Push de l'image vers le registry
  4. Deploy sur le VPS via SSH
  5. Health check post-deploy
  6. Rollback automatique en cas d'echec

Le pattern Docker est simple : chaque app produit un container standalone qui ne depend de rien d'autre en runtime. Le monorepo est un outil de developpement, pas une contrainte de production.

Les ports sont geres via un standard centralise (STANDARDS-ports.md) qui attribue un port unique a chaque app. Un reverse proxy Nginx route le trafic vers les bons containers.

Les defis et les lecons

Build times

Avec 9 apps et 48 packages, le build complet prend du temps. Notre solution : ne builder que ce qui a change. Le systeme de caching de Turborepo (que nous avons evaluate) n'a pas ete retenu car PNPM avec des scripts cibles fonctionne suffisamment bien pour notre echelle.

En pratique, nous ne buildons jamais les 9 apps d'un coup. Un changement dans un package UI ne trigger le rebuild que des apps qui l'utilisent.

Les hooks Git

Avec 9 apps et 48 packages, les hooks pre-commit doivent etre rapides. Nous avons 22 validators dans .githooks/ mais ils ne s'executent que sur les fichiers modifies. Le secret : des scripts Bash optimises qui filtrent par scope avant de lancer les verifications lourdes.

La coordination

Le risque d'un monorepo, c'est le "big bang merge" : une PR qui touche 5 apps et 10 packages. Notre regle : les PR touchent au maximum 2 apps. Si un changement impacte plus, il est decoupe en PRs atomiques.

Les deps circulaires

Avec 48 packages, le risque de dependances circulaires est reel. La regle est simple : la couche infrastructure ne depend jamais de la couche metier. Les packages metier peuvent dependre de l'infrastructure mais pas entre eux. Les apps dependent des packages mais pas les unes des autres.

Est-ce que ca scale

A notre echelle (9 apps, 48 packages, 1 developpeur principal + IA), le monorepo scale parfaitement. L'overhead de maintenance est largement compense par le taux de reutilisation.

Les limites apparaitront probablement autour de 20+ apps et 100+ packages, ou quand l'equipe depassera 10 personnes. A ce stade, un split en federation de repos avec des packages publies pourrait devenir necessaire. Mais nous n'y sommes pas.

Ce que nous recommandons

Si vous construisez un ecosysteme de produits SaaS lies, le monorepo avec PNPM est un excellent choix. Les conditions de succes :

  • Des packages internes bien definis des le depart
  • TypeScript strict comme filet de securite
  • Un deploy autonome par app (pas de deploy monolithique)
  • Des conventions strictes sur les imports et les dependances
  • Des hooks Git pour enforcer la qualite

Le monorepo n'est pas une silver bullet. C'est un multiplicateur. Si vos fondations sont solides, il multiplie votre productivite. Si elles sont bancales, il multiplie votre chaos.

Conclusion

Notre monorepo OmniRealm heberge 9 produits SaaS, 48 packages partages et un taux de reutilisation de 66%. Il nous permet de lancer un nouveau produit en 3 jours au lieu de 3 semaines, avec la meme qualite de code et la meme couverture de tests.

C'est notre avantage competitif technique. Et tout le monde peut faire la meme chose avec les bons outils et les bonnes conventions.