Un seul Stripe pour toute une suite : comment nous avons câblé la facturation multi-produits
Retour d'expérience : centraliser la facturation de plusieurs produits SaaS — un client Stripe unique par organisation, des produits qui ne parlent jamais à Stripe, et les leçons.
Quand on construit une suite de produits SaaS — chez nous : un cœur commun, un produit de carte de visite numérique, un CRM en préparation — la facturation pose une question d'architecture avant de poser une question de paiement : qui parle à Stripe ? Notre réponse : un seul composant, et personne d'autre. Ce retour d'expérience détaille le câblage et les règles qui en découlent — transposables même si vous n'avez qu'un produit et l'ambition d'en avoir deux.
Le problème : la facturation se duplique vite
Le chemin naturel, produit par produit, mène chaque application à gérer son intégration Stripe : ses clés, ses webhooks, ses abonnements. Au deuxième produit, les ennuis commencent : un même client professionnel existe deux fois chez Stripe, les remises croisées (« abonné au produit A, réduction sur le B ») deviennent des bricolages, la TVA et la comptabilité se réconcilient à la main, et chaque produit ré-implémente — avec ses propres bugs — la même logique d'abonnement.
Notre architecture : le billing est un service du cœur
Trois règles structurent notre câblage :
1. Un seul Customer Stripe par organisation cliente, détenu par le cœur de la suite. Les produits ne créent jamais rien chez Stripe ; ils demandent au cœur d'ouvrir un paiement pour telle offre.
2. Les produits lisent des entitlements, jamais Stripe. Le mot mérite définition : un entitlement est un droit applicatif — « cette organisation a l'offre Pro du produit vCard, avec 10 sièges » — stocké chez nous, mis à jour par les webhooks Stripe. Le produit ne se demande jamais « que dit Stripe ? » mais « que disent mes droits ? » :
// Dans un produit : la question n'est jamais posée à Stripe.
const droits = await entitlements.get(organisationId, 'vcard');
if (!droits.features.includes('custom_templates')) {
throw createError({ statusCode: 402, message: 'Offre supérieure requise' });
}
3. La vérité voyage par webhooks signés, dans un seul sens. Stripe notifie le cœur (paiement réussi, abonnement modifié, échec de prélèvement) ; le cœur met à jour les entitlements et redistribue l'événement aux produits concernés, signé, avec livraison « au moins une fois » et idempotence par identifiant d'événement — les doublons sont inoffensifs, l'ordre n'est pas garanti, et le code en tient compte.
Ce que cette centralisation nous a donné
Le bénéfice attendu : une intégration Stripe écrite une fois, testée une fois, supervisée une fois. Les bénéfices moins prévus : les offres combinées et remises de fidélité entre produits deviennent une configuration de catalogue plutôt qu'un développement ; et le support gagne un réflexe précieux — toute question de facturation a une source de vérité, consultable dans notre back office.
Les leçons payées (deux, cash)
Un abonnement se modifie, il ne se rachète pas. Notre premier flux de changement d'offre rouvrait une session de paiement — résultat découvert en test réel : un client qui passait de Pro à Team se retrouvait avec deux abonnements actifs. La correction : un changement de plan modifie l'abonnement existant (Stripe gère alors les prorata) au lieu d'en créer un second. Leçon d'architecture : le tunnel de paiement sert à entrer ; tout le reste de la vie contractuelle passe par la modification.
Un webhook non testé en dev est un webhook cassé en prod. Pendant des semaines, nos environnements de développement n'écoutaient pas réellement les webhooks Stripe — les paiements de test « marchaient », les entitlements ne se mettaient à jour que par des chemins de contournement. Le jour où l'écart a été identifié, la règle est devenue : l'outil d'écoute des webhooks démarre avec l'environnement de dev, pas sur demande. Nous avons depuis généralisé le principe bien au-delà de Stripe — il a désormais son propre article dans ces coulisses.
Les limites de ce modèle
Honnêteté d'architecte : cette centralisation crée un point de passage obligé. Si le service de billing du cœur tombe, aucun produit ne peut vendre — nos produits continuent de fonctionner (les entitlements sont lus localement, en cache), mais pas de facturer. À notre échelle, c'est un compromis rationnel ; une fédération de produits vraiment autonomes, avec des équipes séparées, pourrait préférer l'inverse. Et ce modèle suppose une notion d'« organisation » partagée entre produits — si vos produits n'ont pas de clients communs, la mutualisation perd son principal intérêt.
Ce qu'il faut retenir
Si plusieurs produits — ou l'ambition d'en avoir — partagent des clients : un seul compte client de paiement par organisation, une seule intégration au prestataire de paiement, et des produits qui consomment des droits applicatifs plutôt que l'état du prestataire. Et deux règles gagnées à la dure, valables pour tous : modifier un abonnement n'est pas en recréer un, et un webhook qui n'a jamais tourné en dev n'existe pas.
Deux articles voisins.
Vous avez le même problème ?
C'est souvent comme ça que les missions commencent. Décrivez votre contexte, on vous dit ce qu'on en pense sous 24 h.
Décrire votre projet