Donner à chaque client SaaS son propre espace de contenu
Donnez à chaque client un espace de contenu isolé, avec une isolation assurée par la sécurité au niveau des lignes de PostgreSQL — et non par une instruction if de couche applicative.
Quelque part entre le vingtième et le cinquantième client, « on ajoutera juste une colonne tenant_id » cesse d'être un plan et devient un risque.
Cet article traite des trois façons de gérer du contenu multilocataire, de la raison pour laquelle l'une d'elles est nettement plus sûre, et de ce que les modèles de tarification courants des CMS font à la marge d'un SaaS quand le nombre de clients augmente.
Presque tout le monde finit au niveau des lignes, parce que le coût d'exploitation des deux autres est réel et cumulatif. Ce qui fait de la dernière case toute la question : qu'est-ce qui arrête la requête qui oublie le filtre ?
Non parce que votre équipe est négligente. À cause de l'arithmétique.
Un produit mature comporte des centaines de requêtes. Certaines sont dans la tâche de reporting que personne n'a ouverte depuis un an. Certaines sont dans l'outil d'administration écrit pendant un incident. Certaines sont dans un module qu'un prestataire a ajouté et qu'un relecteur a survolé. Chacune d'elles doit se souvenir de WHERE tenant_id = $1.
L'échec est silencieux et total. Un filtre manquant ne lève pas d'erreur. Il renvoie le contenu d'un client à un autre, dans une réponse qui paraît parfaitement normale jusqu'à ce que quelqu'un remarque la page tarifaire non publiée de son concurrent dans une liste déroulante.
La sécurité au niveau des lignes de PostgreSQL déplace la frontière sous l'erreur. Vous attachez une politique à la table, définissez le locataire dans la session, et la base de données applique le prédicat à chaque requête — y compris celles que personne n'a relues :
Désormais, la requête qui a oublié le filtre renvoie zéro ligne au lieu de celles de tout le monde. Le bogue devient une absence visible plutôt qu'une fuite invisible — et cette différence, c'est tout l'argument.
corpusctl fait cela dans le cœur, aux côtés de l'authentification et du registre des modules. Chaque requête applicative reste explicitement délimitée par locataire ; la RLS est la deuxième ligne de défense, pas la première. Tout l'intérêt d'une deuxième ligne, c'est qu'elle est là le jour où la première cède.
Les tables sont aussi partitionnées par hachage selon le locataire, ce qui empêche un gros client de dégrader les plans de requête de tous les autres.
L'erreur de modélisation qui coûte le plus cher par la suite.
Dans un vrai SaaS, les personnes appartiennent à plusieurs clients. Un utilisateur d'agence gère trois de vos clients. Un ingénieur de support a besoin d'un accès en lecture à un compte pendant une semaine. Un prestataire travaille sur la marque deux et ne doit pas voir la marque une.
Si votre modèle est « l'utilisateur appartient au locataire », chacun de ces cas devient un compte en double, et les comptes en double deviennent un problème d'audit.
Le modèle de corpusctl : un utilisateur porte un rôle distinct dans chaque espace. Propriétaire sur l'un, éditeur sur un autre, invisible sur un troisième. Une identité, N adhésions, et le panneau n'affiche que les espaces auxquels la personne appartient réellement.
Il y a une règle délibérée par-dessus : le dernier propriétaire d'un espace ne peut être ni rétrogradé ni retiré. Un espace sans propriétaire est un espace que personne ne peut administrer — le verrou reste à l'extérieur.
#Ce que la tarification par projet fait à la marge d'un SaaS
C'est ici que le choix du CMS cesse d'être une décision technique.
Vérifié sur les pages de tarifs des éditeurs le 17 août 2026 :
Produit
Unité
Prix
100 clients
Strapi Cloud
Par projet
à partir de 35 $/mois
à partir de 3 500 $/mois
Sanity
Par jeu de données au-delà de 2
999 $/mois
prohibitif
Directus
Par instance / siège
499 $/mois Team, +50 $/siège
exploitation par instance
Ghost(Pro)
Par publication, échelonné selon l'audience
à partir de 18 $/mois à l'année
100 abonnements
corpusctl
Aucune
Forfait fixe
inchangé
Le schéma à remarquer : ces unités évoluent avec votre nombre de clients, pas avec votre revenu par client. Un SaaS sur un forfait à 29 $/mois ne peut pas absorber une ligne CMS à 35 $/mois par client. L'arithmétique ne fonctionne tout simplement pas, et elle ne s'améliore pas avec le volume.
C'est le cas pour lequel corpusctl a été conçu. Les espaces ne sont pas une unité de facturation.
#Les lectures, et pourquoi elles non plus ne devraient pas être facturées
Le second coût qui évolue avec les clients, c'est le trafic. Si votre CMS facture les appels d'API, alors les visiteurs de chaque client sont sur votre compteur, et votre client le plus actif est votre plus coûteux — quel que soit ce qu'il vous paie.
Le chemin de lecture de corpusctl supprime le compteur en supprimant l'appel. La publication écrit le contenu dans un fichier immuable, adressé par hachage dans le stockage objet et rafraîchit un manifeste. Le client du lecteur résout le slug vers un fragment de manifeste localement, puis récupère cette adresse depuis le cache de périphérie nginx. L'adresse est un hachage de contenu, elle ne peut donc jamais renvoyer un contenu différent — mise en cache pendant trente jours sans stratégie d'invalidation.
Le trafic des visiteurs n'atteint jamais l'API ni PostgreSQL. Votre chemin d'écriture est dimensionné pour vos éditeurs ; votre chemin de lecture est dimensionné par votre cache.
#Les rétrogradations ne doivent pas supprimer de données
Dans un SaaS, vous aurez des clients qui rétrogradent, se désabonnent et reviennent. Ce que le CMS fait à ce moment-là est une décision produit dont vous héritez.
corpusctl fait de chaque fonctionnalité un module — médias, recherche, webhooks, SEO, GraphQL, import, analytique — avec deux états :
État
Données
disabled
Conservées. Réactivez-le et il reprend là où il s'était arrêté.
uninstalled
onUninstall les efface, avec une double confirmation.
Une rétrogradation de forfait met les modules à l'état disabled. Elle ne supprime jamais de données. C'est imposé dans le code, pas promis dans un article de support — ce qui compte, car le client qui revient au bout de trois mois s'attend à retrouver son contenu, et il a raison.
Les modules ne peuvent pas non plus s'importer les uns les autres. Un registre de services et un bus d'événements sont les seuls canaux entre eux, et la CI fait échouer la compilation si cette règle est enfreinte. La conséquence pratique : retirer une fonctionnalité ne peut pas casser le système, si bien que les ensembles de fonctionnalités par locataire sont une question de configuration plutôt qu'un fork.
#Une liste de contrôle pour évaluer tout CMS pour un SaaS multilocataire
Un projet est-il une unité de facturation ? Combien coûte le 100e client ?
Où l'isolation est-elle imposée — dans l'application ou dans la base de données ?
Un même utilisateur peut-il détenir des rôles différents dans différents locataires ?
Les lectures sont-elles facturées ? Par qui — vous, ou les visiteurs de vos clients ?
Qu'advient-il des données lors d'une rétrogradation ?
Pouvez-vous provisionner un locataire depuis l'API, sans intervention humaine dans le panneau ?
Existe-t-il un compteur de quota par locataire que vous pouvez lire et facturer ?
La première question élimine la plupart des candidats avant même d'arriver à la deuxième.
Un CMS headless qui épouse Next.js au lieu de le combattre
Le mode brouillon, l'ISR, l'App Router et la couche de rendu. Ce qui compte vraiment quand vous branchez un CMS headless à Next.js — et les trois façons dont ça tourne mal.