Une base par client, ou une base pour tous ?
C’est une des rares décisions d’architecture qu’on ne peut pas repousser. Le jour où la première application multi-clients reçoit son deuxième client, le choix est fait, qu’on l’ait pris consciemment ou non.
Deux modèles, et ils ne se valent pas selon ce qu’on vend.
Les deux modèles
Une base pour tous. Chaque table porte une colonne clientId, et chaque
requête la filtre. C’est le modèle par défaut de la plupart des applications SaaS,
et il est bon marché : une base, une migration, une sauvegarde.
Une base par client. Chaque client a son schéma ou sa base. Les requêtes n’ont rien à filtrer, parce qu’il n’y a rien à mélanger.
Sur Beacon, la plateforme de gestion de communauté que je vends, chaque communauté a son instance et sa base. Ce choix ne vient pas d’un goût pour la pureté, il vient de ce que le produit promet, et j’y reviens plus bas.
Le prix de la base partagée
Le coût n’est pas là où on l’attend. Ce n’est pas la performance : un index sur
(clientId, …) tient très bien jusqu’à des volumes confortables.
Le coût est dans l’oubli. Il suffit d’une requête écrite sans le filtre pour qu’un client voie les données d’un autre. Une seule, dans une route secondaire, écrite un vendredi. Les défenses existent, et la meilleure est la sécurité au niveau de la ligne de PostgreSQL, qui déplace le filtre de votre code vers le moteur :
ALTER TABLE ticket ENABLE ROW LEVEL SECURITY;
CREATE POLICY isolation ON ticket
USING (client_id = current_setting('app.client_id')::uuid);
À condition de poser app.client_id sur chaque connexion, de ne jamais se
connecter avec un rôle qui contourne la politique, et de se rappeler que
BYPASSRLS est attribué au propriétaire de la table. Trois conditions, et il
suffit qu’une seule tombe.
Le deuxième coût est la restauration. Un client vous appelle : il a supprimé une catégorie entière hier, il veut la retrouver. Sur une base partagée, votre sauvegarde contient les données de tous les autres clients, qui ont continué à travailler depuis. Vous ne pouvez pas restaurer la base. Vous devez extraire les lignes d’un client d’un cliché, les réinjecter en gérant les clés étrangères et les séquences, sans toucher au reste. Cette procédure s’écrit, se teste, et elle demande une demi-journée la première fois.
Sur une base dédiée, c’est une commande.
Le troisième coût est réglementaire. Un membre demande l’effacement de ses
données. Sur une base partagée, il faut parcourir chaque table, trouver ses
lignes, décider de celles qui doivent survivre, et prouver que rien n’a été
oublié. Le travail est le même dans les deux modèles, mais le périmètre à
parcourir n’a pas la même taille, et l’export d’un client se fait par pg_dump
plutôt que par un script à écrire.
Le prix de la base dédiée
Il faut le dire, parce que c’est le côté qu’on vend habituellement sans ses contreparties.
Les migrations se multiplient. Une modification de schéma ne s’applique plus une fois, mais autant de fois qu’il y a de clients, et il faut savoir où elle en est pour chacun. Sur Beacon, le point d’entrée de chaque instance applique les migrations manquantes à son démarrage, et un client qui redémarre trois jours après les autres se met à niveau tout seul. Sans cette mécanique, on découvre les écarts en production.
Les connexions coûtent. PostgreSQL alloue de la mémoire par connexion. Cent
bases avec chacune son pool, c’est cent pools. Un pgbouncer en amont devient
obligatoire plus tôt qu’on ne le croit.
Les requêtes transversales deviennent pénibles. « Combien de tickets ouverts sur l’ensemble du parc » est une jointure triviale sur une base partagée, et un agrégat à construire sur cent bases. Si votre produit a besoin de ce genre de chiffre en temps réel, le modèle dédié vous gêne tous les jours.
Au-delà de quelques centaines de clients, le modèle dédié cesse d’être raisonnable sans une automatisation complète du montage, de la mise à jour et de la supervision. C’est un investissement à part entière, pas un effet de bord.
Trois questions pour trancher
Trois questions suffisent à trancher, dans cet ordre.
Vos clients achètent-ils des fonctions différentes ? Si oui, la base dédiée devient presque obligatoire. Sur Beacon, un client qui n’a pas acheté le module de billetterie n’a pas les tables correspondantes dans sa base. Une base partagée aurait forcé à créer toutes les tables pour tout le monde et à cacher les fonctions par des droits, ce qui n’est pas la même promesse.
Que se passe-t-il si un client voit les données d’un autre ? Sur une application de gestion interne, c’est un incident. Sur une application qui manipule des données de santé, des données de paiement ou des sanctions nominatives, c’est une fin. Plus la réponse est grave, plus la base dédiée se justifie.
Combien de clients dans trois ans ? Dix, la question ne se pose pas, prenez le modèle dédié. Dix mille, elle ne se pose pas non plus, prenez le modèle partagé et investissez dans la sécurité au niveau de la ligne. Entre les deux, c’est un vrai arbitrage, et il dépend surtout des deux réponses précédentes.
Sur Beacon, le dédié a coûté plus cher que prévu
Sur Beacon, le modèle dédié a coûté plus cher que prévu, et pas là où je l’attendais. Ce n’est pas le montage d’une instance qui prend du temps, c’est la mise à jour du parc : il a fallu écrire la livraison par vagues, le retour arrière, et un contrôle qui vérifie après coup que chaque instance tourne bien la version annoncée. Trois semaines de travail qui ne se voient sur aucune page de vente.
En contrepartie, la promesse « vos données ne croisent celles de personne » n’a pas d’astérisque, et un client qui part emporte un fichier que je produis en une commande. Sur ce produit-là, c’était le bon échange. Sur un outil interne vendu à trois services de la même entreprise, ça ne l’aurait pas été.