J'ai arrêté de construire des sites — je construis une usine à navires
strategie

J'ai arrêté de construire des sites — je construis une usine à navires

Monorepo PrestaShop Headless transformé en PaaS souverain single-tenant : EventBus, Constitution Industrielle, déploiement client en 120s.

8 min de lecture

Il y a six mois, je construisais des sites e-commerce. Un par un. À la main. Chaque nouveau client signifiait un nouveau repo, un nouveau cycle de copier-coller, et trois semaines de dette technique accumulée avant même la mise en production.

Aujourd’hui, je déploie un client en marque blanche en 120 secondes. Un fichier de configuration. Un deploy. C’est en ligne.

Ce n’est pas de la magie. C’est de l’ingénierie industrielle appliquée au logiciel vertical. Voici comment j’y suis arrivé — et pourquoi cette architecture est devenue mon avantage concurrentiel le plus difficile à copier.


Le piège des deux extrêmes

Cet article fait partie de notre dossier Stratégiearchitecture.

L’illusion du SaaS multi-tenant

Le réflexe de l’industrie, c’est le SaaS multi-tenant. Une base de données partagée. Un client_id partout. Une seule instance qui sert tout le monde.

Sur le papier, c’est élégant. En pratique, c’est un cauchemar qui s’aggrave avec chaque client :

  • Fuite de données — un WHERE client_id = ? oublié et les données d’un client apparaissent chez un autre. J’ai vu des plateformes SaaS perdre des contrats à sept chiffres pour ça.
  • Performance dégradée — le client qui fait un export de 50 000 produits ralentit tout le monde.
  • Complexité exponentielle — chaque feature doit être pensée multi-tenant. Chaque migration doit être rétrocompatible pour tous les clients simultanément.
  • Souveraineté impossible — vos données vivent sur le serveur de quelqu’un d’autre, mélangées avec celles de vos concurrents.

L’artisanat qui ne scale pas

L’alternative ? Le développement sur-mesure. Un repo par client. Un serveur par client. Mais aussi : un copier-coller par client. Une dette technique par client. Un cauchemar de maintenance qui grandit linéairement.

Au troisième client, vous passez plus de temps à maintenir qu’à construire. Au cinquième, vous êtes paralysé.


La troisième voie : le PaaS Souverain

J’ai choisi de ne choisir ni l’un ni l’autre. J’ai construit un PaaS single-tenant — un modèle où chaque client possède son propre VPS, sa propre base de données, son propre moteur. Mais tous partagent le même Blueprint : un core immuable qui est la source de vérité du code métier.

┌─────────────────────────────────────────────────┐
│                  MONOREPO                       │
│                                                 │
│   core/          ← Le Blueprint (immuable)      │
│   ├── server/api/     APIs métier               │
│   ├── components/     UI partagée               │
│   ├── composables/    Logique Vue               │
│   ├── operations/     EventBus + handlers       │
│   └── config/         Feature flags             │
│                                                 │
│   clients/                                      │
│   ├── client-alpha/   ← Config déclarative      │
│   │   ├── nuxt.config.ts  (22 lignes)           │
│   │   ├── assets/                               │
│   │   └── public/                               │
│   ├── alexandrecarette/                         │
│   └── [prochain-client]/                        │
└─────────────────────────────────────────────────┘
         │                          │
         ▼                          ▼
   ┌──────────┐             ┌──────────┐
   │  VPS #1  │             │  VPS #2  │
   │ Alpha    │             │ Client X │
   │ DB isolée│             │ DB isolée│
   └──────────┘             └──────────┘

Un VPS = Un client = Une DB. Pas de client_id. Pas de données mélangées. Pas de voisin bruyant. Chaque instance est souveraine.


Le Business OS : un système nerveux événementiel

Le cœur du Blueprint, c’est ce que j’appelle le Business OS — un bus d’événements léger construit sur le EventEmitter natif de Node.js, encapsulé dans une interface métier typée.

Pourquoi un EventBus et pas des appels directs ?

Parce qu’un site e-commerce n’est pas une suite de pages. C’est un flux d’événements métier : un devis est demandé, une commande est passée, un paiement est reçu. Chaque événement peut déclencher plusieurs réactions indépendantes — et ces réactions ne doivent jamais se bloquer mutuellement.

Voici l’interface :

// core/server/operations/bus/EventBus.ts

export interface DomainEvent<T = unknown> {
  id: string        // UUID v4
  name: string      // 'quote.requested'
  timestamp: string // ISO 8601
  payload: T        // Données typées
}

class EventBus {
  private emitter = new EventEmitter()

  publish<T>(event: DomainEvent<T>): void {
    for (const handler of this.emitter.listeners(event.name)) {
      try {
        const result = (handler as EventHandler<T>)(event)
        if (result instanceof Promise) {
          result.catch((err) =>
            console.error(`[EventBus] '${event.name}':`, err?.message)
          )
        }
      } catch (err: any) {
        console.error(`[EventBus] '${event.name}':`, err?.message)
      }
    }
  }

  subscribe<T>(eventName: string, handler: EventHandler<T>): void {
    this.emitter.on(eventName, handler as EventHandler)
  }
}

export const eventBus = new EventBus()

Fire-and-forget, résilient par design

Quand un prospect soumet une demande de devis, l’API publie un événement et rend la main immédiatement :

// core/server/api/quote/submit.post.ts

export default defineEventHandler(async (event) => {
  const data = QuoteSubmitSchema.safeParse(body).data

  // Publier → les handlers s'exécutent en parallèle
  eventBus.publish(createQuoteRequestedEvent({
    ...data,
    totalItems: data.items.reduce((s, i) => s + i.quantity, 0),
  }))

  return { success: true }
})

En coulisses, deux handlers se déclenchent simultanément — sans se connaître, sans se bloquer :

// core/server/plugins/operations.ts

eventBus.subscribe(QUOTE_REQUESTED, SaveToDatabaseHandler)
eventBus.subscribe(QUOTE_REQUESTED, PushToCrmHandler)

Si le CRM est en panne, la sauvegarde en base fonctionne quand même. Si la base est lente, le CRM reçoit sa notification sans attendre. Un handler en erreur ne contamine jamais les autres.

Et parce que chaque VPS est isolé, l’EventBus reste local. Pas de Redis. Pas de RabbitMQ. Pas d’infrastructure distribuée à maintenir. Le jour où un client aura besoin de plus, la migration vers une file d’attente externe ne changera pas une ligne dans les handlers — juste l’implémentation du bus.


La Constitution Industrielle

Un moteur partagé entre plusieurs clients, c’est une bombe à retardement si n’importe qui peut y écrire n’importe quoi. J’ai donc verrouillé le système avec quatre règles constitutionnelles.

Règle 1 — Le Core est immuable

core/           ← TOUTE la logique métier vit ici
  server/api/   ← APIs
  components/   ← UI
  composables/  ← Logique réactive
  operations/   ← EventBus + handlers

Aucune exception. Si un client a besoin d’une feature, elle est codée de manière générique dans le core et activée par configuration. Le core est le Blueprint — il est versionné, testé, et identique sur tous les VPS.

Règle 2 — La config est 100 % déclarative

Un client, c’est un fichier nuxt.config.ts de 22 lignes :

// clients/client-alpha/nuxt.config.ts

export default defineNuxtConfig({
  extends: ['../../core'],

  runtimeConfig: {
    public: {
      clientId: 'client-alpha',
      brandName: 'Mon Enseigne',
      contactEmail: 'contact@mon-enseigne.fr',
      b2bMode: true,
      catalogueIndexable: true,
    },
  },
})

extends: ['../../core'] — cette ligne hérite de tout : les APIs, les composants, les composables, les pages, les assets. Le client ne déclare que ce qui le différencie.

Règle 3 — Zéro hardcoding client

Interdit d’écrire if (client === 'client-alpha') dans le code. Jamais. La différenciation passe exclusivement par :

  • Les variables d’environnement (.env par VPS)
  • Les feature flags (runtimeConfig.public.b2bMode)
  • Les surcharges UI (un composant dans clients/client-alpha/components/ remplace celui du core)

Le core ne sait pas pour qui il tourne. Il sait juste ce qu’il doit faire.

Règle 4 — Chaque base est souveraine

Pas de colonne client_id. Pas de WHERE tenant = ?. La requête SQL est la même partout — c’est la base de données qui change.

-- Cette requête est identique sur tous les VPS
SELECT * FROM ps_product WHERE active = 1

-- Client A → DB Client A (ses 2 000 produits)
-- Client X → DB Client X (ses 500 produits)
-- Aucune possibilité de fuite de données

Les résultats

Cette refactorisation n’était pas un exercice académique. Voici ce qu’elle a produit en une session :

Métrique Avant Après
Lignes de dette technique ~400 lignes de code mort, shims, résidus Purgées
Temps de build Variable, dépend du tenant ~120 secondes
Fichiers dans clients/client-alpha/ Logique métier mélangée Config + assets uniquement
Déploiement nouveau client Jours de setup Un nuxt.config.ts + un deploy
Risque de fuite inter-client Structurel (DB partagée) Impossible (DB isolées)

La réduction de 400 lignes n’est pas anodine. Chaque ligne supprimée est une ligne qui ne peut plus casser en production, qui n’a plus besoin d’être maintenue, qui ne ralentit plus la compréhension du code.


Le Moat : pourquoi c’est difficile à copier

Ce système est un avantage concurrentiel composé. Voici pourquoi :

1. Le coût marginal d’un client tend vers zéro. Quand le Blueprint est stable, ajouter un client coûte un fichier de config, un VPS à 20 €/mois, et un deploy. La marge augmente à chaque nouveau client sans augmenter la complexité.

2. Chaque amélioration bénéficie à tous les clients. Un fix de performance dans le core ? Tous les VPS en profitent au prochain déploiement. Un nouveau composant ? Disponible partout. L’effort est mutualisé, le bénéfice est multiplié.

3. L’isolation rend le système antifragile. Un client qui fait une bêtise ne peut pas impacter les autres. Un VPS qui tombe ? Les autres continuent. Pas de single point of failure, pas de “noisy neighbor”.

4. La Constitution empêche la régression. Les quatre règles ne sont pas des guidelines — ce sont des gardes-fous automatisés. Les hooks pre-commit vérifient qu’aucune logique métier n’atterrit dans clients/. Les feature flags empêchent le hardcoding. Le système se protège lui-même.

Ce n’est pas un framework. Ce n’est pas un template. C’est une discipline industrielle encodée dans l’architecture — et c’est exactement ce qui la rend difficile à reproduire. Vous pouvez copier le code. Vous ne pouvez pas copier les centaines de décisions qui ont mené à ces règles.


Pour qui c’est fait

Si vous êtes un CTO ou un directeur e-commerce qui gère (ou prévoit de gérer) plusieurs boutiques B2B — marques sœurs, filiales, marchés internationaux — cette architecture résout le problème que vous connaissez déjà : comment scaler sans que chaque nouvelle boutique devienne un projet à part entière.

Si vous êtes un développeur PrestaShop qui en a marre de copier-coller des modules entre des instances, vous savez exactement de quelle douleur je parle.

Et si vous êtes un artisan du logiciel qui croit que l’excellence technique et la rentabilité ne sont pas incompatibles — bienvenue. On construit la même chose.


Questions fréquentes

Tout ce que vous devez savoir sur ce sujet.

Une question ?

Contactez-nous directement.

Gratuit & sans engagement — réponse sous 24h

Discussion

Votre avis sur cet article

Les commentaires sont modérés et répondus par une intelligence artificielle. Votre email ne sera jamais affiché.

0 / 2000

En publiant, vous acceptez que votre nom et commentaire soient affichés publiquement. Votre email est utilisé uniquement pour la modération (base légale : intérêt légitime, durée : 3 ans). Politique de confidentialité.