Architecture Logicielle

Dans l'Architecture de WebOustaou : un Monorepo Construit pour se Scinder

Pourquoi WebOustaou est trois applications Next.js et quinze packages internes plutôt qu'une seule, comment un test à la compilation impose la frontière, et une abstraction que j'ai livrée, ratée, puis délibérément retirée.

21 juil. 2026

Une Application, Puis Trois

WebOustaou n'a pas commencé comme un monorepo. Il a commencé comme une seule application Next.js, et pendant un temps c'était le bon choix — une équipe, une base de code, une seule chose à déployer. Ça a cessé d'être le bon choix le jour où une modification d'une ligne de texte sur la page marketing publique s'est mise à compiler avec elle toute la surface Stripe, Supabase, le tracking QR et l'éditeur de texte riche. Le build de production pour une poignée de pages marketing avait gonflé à environ 600 Mo pour quelques mégaoctets de code source réel.

La solution n'était pas une réorganisation de dossiers. C'était de reconnaître que "site marketing" et "back-office authentifié" ont des besoins de dépendances, des rythmes de déploiement et des rayons d'impact fondamentalement différents — et qu'aucune discipline interne ne tient cette ligne sans que le système de build ne l'impose. L'application s'est donc scindée en trois :

Le tableau de bord admin — la surface authentifiée avec l'ensemble complet des dépendances

  • marketing — le site public. Aucun client Supabase, aucun SDK Stripe, aucun code d'authentification ne peut y être compilé, un point c'est tout.
  • admin — le back-office authentifié, portant l'ensemble complet des dépendances : Supabase, Stripe, tracking QR, éditeur de texte riche.
  • site-template — un site consommateur mono-tenant, destiné à être dupliqué par client plutôt que partagé.

La frontière n'est pas une convention écrite dans un README — c'est un test de fermeture de dépendances exécuté à la compilation, qui fait échouer le build si marketing importe un jour quelque chose qu'il ne devrait pas. C'est la différence entre une décision d'architecture et un souhait d'architecture.

Multi-Tenant Sans Multi-Déploiement

Changement d'établissement dans l'application admin

Sous les trois applications se trouve un seul projet Supabase modélisant une hiérarchie à trois niveaux : tenants → shops → websites. Un tenant est l'entité de facturation et de propriété ; un shop est un établissement physique ; un website est une vitrine publique, et un shop peut en exposer plusieurs. La sécurité au niveau des lignes (RLS) est activée sur chaque table, refus par défaut, dès la toute première migration — avant même qu'il n'existe le moindre système d'authentification pour l'appliquer. Cet ordre compte : rajouter le RLS sur un schéma existant est un audit de sécurité complet ; l'avoir dès le début est une contrainte de conception avec laquelle on construit dès le premier jour.

Décider où Vit le Contenu

L'un des débats internes les plus intéressants portait sur l'endroit où le contenu devait réellement vivre : entièrement dans Supabase (tout éditable, rien de statique), entièrement dans le code (rapide, simple, mais chaque ajustement de texte marketing nécessite un déploiement), ou un partage des deux. L'équipe a opté pour un modèle par paliers : les données opérationnelles — menu, horaires, fermetures — vivent dans Supabase car elles changent souvent et nécessitent un éditeur accessible au propriétaire. Le contenu marketing — texte du hero, page à propos, propositions de valeur — vit en JSON statique dans le dépôt de chaque client, car il change rarement et transformer chaque modification de titre en formulaire CMS aurait résolu un problème que personne n'avait.

Cette décision n'était pas théorique — elle correspondait à la façon dont le tout premier site client était déjà organisé, ce qui est généralement un bon signe qu'une décision de conception est juste et pas seulement élégante.

Construire la Mauvaise Abstraction, Puis la Corriger

Toutes les décisions ne tiennent pas la route, et les plus utiles à raconter sont celles qui n'ont pas tenu. Un package antérieur, site-blocks, était censé contenir des briques réutilisables et agnostiques du framework — un header, un footer, un hero. Avec le temps, il a discrètement fini par posséder tout le site visuel : chrome, thématisation, interactivité, composition — pendant que le template propre à chaque client devenait un simple relais de props autour de lui. C'est l'inverse de ce qui était prévu. Le template est censé posséder la composition ; les packages sont censés posséder les primitives et les données.

La correction a été un reset délibéré, formalisé comme une fiche de décision d'architecture plutôt que comme un refactor silencieux : site-blocks a été purement et simplement déprécié, ses éléments utiles (un système de thématisation, un point d'ancrage pour la bannière de consentement) absorbés ailleurs, et la frontière réécrite ainsi : "les packages possèdent les données et les primitives, le template possède la page complète." Un package lié, tokens, a été déprécié de la même façon après qu'un audit a révélé qu'il avait été publié et versionné pendant des mois sans que le site-template ne l'importe jamais réellement — le mécanisme réel pour l'image de marque par client s'est avéré être un fichier brand/palette.css écrit à la main par fork, et non un package de design tokens partagé. Déprécier un package qu'on a construit et livré est une histoire moins confortable à raconter qu'en livrer un nouveau, mais c'est la plus honnête.

Scinder la Couche de Données par Framework, pas par Fonctionnalité

La couche de données publique a connu un raffinement similaire. Elle a commencé comme un unique package consumer-data effectuant des requêtes PostgREST anonymes en lecture seule — utilisant délibérément le fetch natif plutôt que le client supabase-js, afin de rester utilisable depuis une edge function ou une future couche de cache sans embarquer de poids inutile. Quand un site client a eu besoin d'intégration React Query — mise en cache, hydratation, annulation de requêtes — cette logique est allée dans un package séparé, consumer-query, en couche au-dessus, plutôt que d'être intégrée à consumer-data lui-même. Cette scission permet au contrat de récupération de données de base de rester importable dans des contextes qui n'ont jamais entendu parler de React.

Rembourser la Dette Avant que la Phase Suivante n'en Ait Besoin

La décision d'architecture dont je suis le plus fier est la moins visible. Le menu, les horaires et les fermetures étaient à l'origine rattachés à un website — la vitrine publique sur laquelle atterrit un visiteur. Mais la phase suivante de la roadmap est la gestion des commandes, et à terme un système d'écran de cuisine, et les deux appartiennent à l'établissement physique, pas au site public qui a reçu la commande. Interroger l'état de la cuisine via une indirection website-vers-shop deviendrait fragile dès qu'un établissement exposerait plus d'un site.

Alors, avant que cette phase ne commence, les données ont été rattachées de website_id à shop_id à travers tout le schéma — un remboursement de dette technique délibéré et circonscrit, effectué avant qu'il ne bloque la fonctionnalité suivante, pas pendant. C'est un travail peu glamour, sans changement d'interface visible derrière lui, ce qui est généralement le signe qu'il valait la peine d'être fait.

Le Schéma Sous-Jacent à Tout Cela

Chacune de ces décisions — la scission des applications, le schéma RLS dès le premier jour, la hiérarchisation du contenu, le reset de site-blocks, la scission de la couche de données, le rattachement shop/website — a été consignée dans une courte fiche de décision d'architecture avant d'être livrée : contexte, options envisagées, décision, conséquences. Vingt-cinq existent pour WebOustaou à ce jour. Aucune n'est longue. Toutes font que, six mois plus tard, "pourquoi on a fait comme ça" a une réponse qui n'est pas la mémoire de quelqu'un.

C'est l'habitude concrète à transposer de l'ingénierie systèmes aérospatiale vers un petit produit SaaS : pas l'outillage, mais la discipline de consigner la décision pendant qu'on se souvient encore pourquoi on l'a prise.