Plateforme SaaS B2B

Plateforme SaaS B2B

Next.js 13TurborepoTanstack QueryChakra UI

Contexte

Une plateforme SaaS B2B développée from scratch pour le secteur automobile :

  • 🖥️ CMS spécialisé permettant aux organisations clientes (concessionnaires, promoteurs, sociétés de leasing) de créer, gérer et mettre en ligne leurs véhicules et transactions.
  • 🤝 CRM pour gérer leur activité

Les véhicules sont des objets métier polymorphes consommés de bout en bout par un écosystème d'applications interconnectées, chacune les affichant et les manipulant selon son propre contexte : backoffice de gestion, site public généré dynamiquement, ERP interne, espace client, etc.

J'ai travaillé sur l'ensemble de ces applications pendant 3 ans, seul sur le front pendant plusieurs mois après le lancement du projet.

Modélisation polymorphe

Le cœur du projet reposait sur un modèle de données discriminé et imbriqué. Chaque ressource créée via le CMS n'est pas un objet générique, c'est un objet typé, dont la structure varie selon son discriminant.

Modèle de données polymorphe

Un modèle aplati force l'interface à gérer des propriétés optionnelles (tout est présent, rien n'est garanti). Un modèle polymorphe discriminé par type permet à chaque objet d'exposer uniquement ses propriétés pertinentes, rendant le typage strict, la validation fiable et l'affichage précis, sans condition superflue.

Voici une approximation du modèle :

  • Un véhicule (voiture · motocyclette · camion · etc.) partage une base commune, mais avec des caractéristiques propres à chaque type.
  • Un véhicule possède des conditions commerciales (vente · leasing · location), elles aussi polymorphes.
Exemple de matrice des cas métiers

Ce modèle produit une matrice de cas métier à implémenter explicitement de bout en bout (formulaire de création/mise à jour, validation, affichage dans chaque application). Chaque combinaison est un cas distinct. C'est ce niveau d'exhaustivité qui permet à l'interface de refléter fidèlement chaque nuance du domaine.

Implémentation front

Traduire un modèle discriminé en interface n'est pas qu'une question de conditions if/else. La vraie question architecturale était : introduire la logique discriminante, et comment l'encapsuler pour qu'elle reste maintenable au fur et à mesure que les combinaisons de cas métier augmentent ?

Isolation de la complexité & Composition

L'objectif était de maintenir la logique discriminante au plus près de l'endroit où elle était nécessaire, pour éviter de faire remonter inutilement la complexité jusqu'au sommet de l'arborescence.

Assemblage dynamique d'un composant spécifique

Chaque type de véhicule ou de transaction dispose de ses propres composants, assemblés dynamiquement selon le discriminant.

Dans certains cas (comme la page de détail du véhicule ou la section de caractéristiques du formulaire backoffice) la discrimination opérait au niveau de la page entière, déterminant quelle vue afficher selon le type. Chaque vue combinait ensuite des sections communes et des sections spécifiques via composition.

Typesafety stricte

Les unions discriminées TypeScript garantissent que chaque cas métier est traité exhaustivement. Le compilateur signale toute combinaison non gérée.

Validation contextuelle

Les règles de validation varient selon le type. Un formulaire de leasing ne valide pas les mêmes champs qu'un formulaire de vente (et n'a pas forcément les mêmes règles métier). Les erreurs sont gérées au cas par cas.

Structure monorepo

Une codebase partagée (monorepo) garantissait la cohérence visuelle et comportementale entre les applications : composants UI présentationnels, design system, outils. Chaque application disposait de sa propre couche d'accès aux données (BFF dédié), les types DTO étant générés automatiquement depuis les documentations Swagger via la librairie Orval. Cela éliminait la duplication manuelle tout en maintenant une séparation claire des responsabilités.

Méthodologie & culture d'équipe

Équipe réduite (2 développeurs front, 2 back). Code reviews systématiques, rituels agiles (standup orienté déblocage plutôt que reporting, sprint planning, sprint retrospective), démos aux stakeholders.

Bilan

3 ans sur une plateforme SaaS complète, développée from scratch jusqu'à sa version 2.6. Ce projet m'a appris à :

  • 🧬 Maintenir la cohérence d'un modèle complexe à travers plusieurs applications
  • 🤝 Travailler en équipe avec une vraie culture de qualité (reviews, ownership, démos)
  • 🕵️ Anticiper les décisions architecturales plutôt que de les subir
  • 🗄️ M'exposer aux enjeux backend et à la modélisation de données. Cette curiosité nourrit aujourd'hui mon évolution vers le fullstack