Deux apps Next.js sur deux domaines, une seule API
Pourquoi on a séparé portail et console opérateur en deux apps Next.js, et ce que cette séparation a retiré de la base.
- Architecture
- Multi-tenant
- Auth
Le sujet n’est pas de raconter un split de repo pour lui-même. Le sujet, c’est de savoir quand deux expériences produit doivent devenir deux frontaux distincts pour garder une base lisible.
Quand deux utilisateurs ne vivent plus le même produit, forcer une seule application web à porter les deux finit souvent par diffuser la complexité partout. C’est exactement le moment où il faut se demander si la mutualisation apparente ne coûte pas plus cher que deux frontaux bien posés.
Le problème à résoudre
Le point de départ était une seule application Next.js capable de regarder le host courant pour se comporter tantôt comme un portail client, tantôt comme une console opérateur.
Sur le papier, l’idée paraît économique :
- une seule app
- un seul pipeline
- un seul layout global
- quelques conditions sur le host
Dans les faits, cette logique fuit vite partout :
- middleware
- navigation
- layouts
- gestion de session
- appels API
- tests
La question n’était donc pas “comment gérer deux domaines dans Next.js ?”. La vraie question était : où veut-on que la complexité vive.
La solution retenue
L’architecture finale est simple à décrire :
apps/portalpourcercly.coapps/operatorpouroperator.cercly.co- une seule
operator-api - deux cookies distincts :
tenant_sessionetoperator_session
Le vrai sujet n’est pas le multi-domaines en soi. Le vrai sujet, c’est l’endroit où la complexité vit.
Avec une seule app, la complexité se diffuse partout. Avec deux apps, elle remonte à la frontière entre les clients et l’API.
Pourquoi cette solution tient bien dans ce type de plateforme
Cette solution tient bien parce qu’elle :
- isole naturellement
tenant_sessionetoperator_session - sépare le portail utilisateur du cockpit interne
- permet des déploiements indépendants sans arbitrage permanent
- simplifie les middlewares et la lecture des permissions
- rend les layouts et les routes plus prévisibles
Dit autrement : on n’a pas dupliqué un produit, on a remis chaque expérience dans le bon périmètre.
Comment on l’implémente
Le bon niveau de code à montrer ici n’est pas un gros diff de repo. Ce sont plutôt deux points d’appui très concrets :
- le middleware portal qui lit le cookie et injecte
x-cercly-tenant-accesssur les routes/api/* - les labels Traefik qui routent les deux domaines vers les bons frontaux
Le but n’est pas de montrer beaucoup de code. Le but est de rendre visible le découpage et de montrer où la complexité a été déplacée.
Un exemple de logique qui reste simple une fois le split fait :
if (!tenantSessionCookie) {
return NextResponse.redirect(new URL("/auth/login", request.url));
}
Le point important, ce n’est pas la ligne ci-dessus. C’est le fait qu’elle n’a plus besoin de se demander si elle tourne dans le portail, l’opérateur, ou une troisième variante host-aware.
Ce que cette séparation a retiré de la base
Le gain le plus sous-estimé est souvent négatif : ce qu’on retire.
On a retiré :
- des branches conditionnelles liées au host
- des layouts qui changeaient de rôle selon le domaine
- des cookies qui n’avaient pas la même sémantique selon le contexte
- des tests qui devaient simuler deux produits dans une seule application
Une architecture saine se voit souvent dans la quantité de logique qu’on n’a plus besoin d’écrire.
Ce que j’ai appris en l’implémentant
Le piège est simple : on pense économiser du temps avec une seule app. En pratique, on achète une dette qui fuit dans toute la base.
La phrase importante à garder est celle-ci : séparer en deux apps a coûté un week-end, pas un mois.
Je préfère payer une petite séparation une fois, plutôt que de maintenir pendant des mois un faux monolithe frontend qui n’a plus de vraie cohérence produit.
Quand ne pas faire ce split
Il ne faut pas transformer ce retour d’expérience en règle absolue.
Si les deux expériences partagent réellement :
- la même navigation
- les mêmes permissions
- le même rythme de release
- la même logique de session
alors une seule application peut rester le meilleur choix.
Ici, ce n’était plus vrai. Et une fois ce constat posé, la séparation était la solution la plus lisible.