La question « comment développer un SaaS multi-tenant en PHP ? » revient sur quasiment chaque projet que je cadre. Pourtant, côté francophone, les ressources sérieuses sont rares. Les guides anglophones existent, mais ils s'appuient sur des stacks différentes (Laravel, Rails, Node) et passent vite sur les pièges réels de Symfony et Doctrine. Cet article corrige ce manque. Je détaille les trois stratégies d'isolation possibles, le code concret pour les implémenter, et les arbitrages que j'applique sur les SaaS B2B que je conçois et maintiens. Un SaaS sur-mesure multi-tenant (aussi appelé logiciel métier hébergé en mode SaaS multi-clients, ou multitenancy / multi-tenancy dans la littérature anglophone) est l'un des choix d'architecture les plus structurants d'un projet : il est important de bien l'arbitrer dès le cadrage.
SaaS multi-tenant : de quoi parle-t-on exactement
Un SaaS multi-tenant est une application web unique qui sert plusieurs clients (les tenants) depuis la même infrastructure. Le code est mutualisé, la base de données aussi (en général), et l'isolation entre clients est gérée par l'application. C'est la nature même du modèle SaaS : un coût marginal faible par client supplémentaire, une maintenance centralisée, une mise à jour qui profite à tout le monde en même temps.
À ne pas confondre avec le multi-instance (ou single-tenant), qui instancie une copie complète de l'application pour chaque client. C'est le modèle des ERP on-premise des années 2000. Beaucoup plus cher à exploiter, mais parfois imposé par des contraintes réglementaires fortes (santé, défense, finance grand compte).
Le travail d'architecte SaaS, c'est d'arbitrer deux exigences opposées : comment isoler les données pour qu'aucun tenant ne voie celles d'un autre, et comment mutualiser les ressources pour que l'ajout d'un tenant coûte le moins possible. Tout le reste découle de cet arbitrage. La documentation de référence sur le sujet, indépendante de fournisseur, reste celle de Microsoft qui formalise les modèles de tenancy.
Les 3 stratégies d'isolation pour un SaaS multi-tenant PHP
Il existe trois grandes stratégies pour isoler les tenants entre eux. Par ordre croissant d'isolation, de coût opérationnel et de complexité technique.
| Stratégie | Isolation | Coût par tenant | Scalabilité | Conformité RGPD |
|---|---|---|---|---|
Table unique + colonne tenant_id | Logique (filtres applicatifs) | Très faible | Excellente jusqu'à plusieurs milliers de tenants | Acceptable si filtres robustes et purge ciblée possible |
| Schéma SQL par tenant | Forte (séparation SQL native) | Modérée | Bonne jusqu'à quelques centaines de tenants | Forte, argument tangible vis-à-vis du DPO |
| Base de données par tenant | Très forte (instance dédiée) | Élevée | Limitée par la gestion opérationnelle | Maximale, localisation par pays possible |
Stratégie 1 : table unique avec colonne tenant_id
Toutes les tables métier portent une colonne tenant_id. Chaque requête Doctrine est filtrée applicativement, en général via un SQLFilter activé dès l'identification de l'utilisateur. C'est le modèle le plus simple, le plus économique, et celui que je recommande par défaut pour la grande majorité des SaaS B2B horizontaux (CRM, facturation, gestion documentaire, RH). Le risque principal : le leak applicatif si une requête native oublie le filtre. La parade tient en deux mesures : filtres Doctrine systématiques, plus tests d'intégration qui vérifient explicitement l'isolation entre deux tenants fictifs.
Stratégie 2 : un schéma SQL par tenant
Chaque tenant a son propre schéma (ou base nommée) sur le même serveur MariaDB. Les tables sont identiques mais physiquement séparées. L'isolation est garantie par le moteur SQL, pas par l'application. C'est le modèle de Salesforce historique. Il devient pertinent pour les secteurs sensibles (santé, finance, juridique) où l'on veut un argument fort vis-à-vis du DPO du client, sans payer le coût opérationnel d'une base par tenant. Le piège : les migrations doivent être appliquées schéma par schéma, ce qui complique les déploiements.
Stratégie 3 : une base de données par tenant
Chaque tenant dispose d'une instance de base dédiée, parfois sur un serveur dédié. C'est l'isolation maximale, qui permet aussi des sauvegardes par tenant, des montées de version par tenant, et une localisation géographique par tenant (utile pour la conformité RGPD multi-pays). Le coût opérationnel est élevé : chaque ajout de client demande un provisionnement, et le parc à maintenir grossit linéairement avec le nombre de clients. À réserver aux SaaS qui adressent moins de 100 clients à fort enjeu contractuel.
D'expérience, sur les SaaS B2B que je conçois pour des TPE et des éditeurs, la stratégie 1 couvre 80 % des cas. Pour les 20 % restants, on bascule sur la stratégie 2, rarement sur la stratégie 3.
Implémentation concrète en Symfony et Doctrine
Stratégie 1 : Doctrine SQLFilter pour le tenant_id
Doctrine fournit un mécanisme idéal pour la table unique : les SQLFilter. On définit un filtre qui ajoute automatiquement la condition WHERE tenant_id = :current_tenant_id à toutes les requêtes des entités concernées. Le filtre s'active dès l'identification de l'utilisateur, via un event listener sur la requête HTTP qui lit le tenant depuis le token de sécurité ou la session.
Concrètement, ça veut dire qu'un développeur distrait qui écrirait findAll() sans filtre ne ramènera que les données du tenant courant. C'est la ceinture et les bretelles : le filtre tourne en transparence sur 100 % des requêtes via Doctrine. Trois précautions pratiques : il faut nommer les entités avec une interface marqueur (par exemple TenantAwareInterface), il faut tester le filtre dans des scénarios multi-tenant, et il faut surveiller les requêtes natives DBAL qui contournent l'ORM. Sur ce dernier point, je désactive systématiquement le DBAL natif sauf cas exceptionnel justifié.
Stratégie 2 : multi-connexions Doctrine et résolution dynamique
Pour le schéma par tenant, Symfony et le bundle doctrine/doctrine-bundle supportent nativement plusieurs entity managers. On configure une connexion par tenant, soit statique (les tenants connus sont déclarés en YAML) soit dynamique (une factory instancie la connexion à la volée selon le tenant identifié). C'est l'approche dynamique que je privilégie en pratique, parce qu'elle évite de redéployer à chaque nouveau client.
Un middleware Doctrine injecte la bonne connexion dans le request scope. Les contrôleurs et services utilisent ensuite un EntityManagerInterface qui pointe automatiquement sur le bon schéma. Les tests doivent prévoir un mécanisme de bascule pour vérifier l'isolation entre tenants, et les fixtures de développement doivent installer un schéma par tenant fictif. La complexité monte d'un cran, mais ça reste maîtrisable sur un projet bien structuré.
Stratégie 3 : provisionnement et orchestration par tenant
Pour la base par tenant, le code applicatif ressemble fortement à la stratégie 2, mais l'infrastructure se complexifie. Chaque création de client déclenche un script de provisionnement : création de l'instance, du schéma, de l'utilisateur SQL avec droits limités, des credentials chiffrés en vault. J'écris en général une commande Symfony dédiée (app:tenants:provision) qui orchestre l'ensemble, avec rollback automatique si une étape échoue. Et un cron de supervision qui vérifie en permanence la santé de chaque instance.
Gestion des migrations multi-tenant
Sur la stratégie 1, les migrations Doctrine s'appliquent une seule fois, comme sur un projet mono-tenant. Pas de difficulté particulière. Sur les stratégies 2 et 3, chaque migration doit être appliquée à chaque tenant. Si vous avez 200 tenants et qu'une migration prend 30 secondes, l'ensemble prend 100 minutes en série. C'est gérable, mais ça change la nature du déploiement.
Ma méthode : une commande Symfony dédiée, type app:tenants:migrate, qui parcourt les tenants et applique les migrations en mode transactionnel, en journalisant chaque exécution. Le script lance les migrations par lots (10 ou 20 tenants à la fois), valide chaque lot, et permet d'arrêter proprement en cas d'incident. La documentation officielle Doctrine Migrations décrit les API qu'on appelle programmatiquement depuis ce type de commande.
Sur les SaaS critiques, j'ajoute une étape de dry run avant les migrations réelles : on génère le SQL sans l'exécuter, on le revoit, on l'applique manuellement sur un tenant test, puis seulement on lance la migration sur le parc. Ça paraît lourd, mais ça évite la migration qui casse 50 clients d'un coup.
Sécurité d'un SaaS multi-tenant PHP
La sécurité d'un SaaS multi-tenant tient sur trois piliers : isolation horizontale entre tenants, chiffrement des données sensibles, et sauvegardes par tenant. L'OWASP A01 (Broken Access Control) est la première cause d'incidents critiques sur les SaaS multi-tenant, justement parce qu'un défaut d'isolation peut exposer les données de plusieurs clients d'un coup.
Isolation horizontale : Voters Symfony et filtres Doctrine
Au-delà du filtre Doctrine qui imposent le tenant_id sur les requêtes, j'ajoute systématiquement des Voters Symfony qui vérifient explicitement que la ressource appartient au tenant courant, sur chaque action sensible. L'attribut #[IsGranted] sur les contrôleurs déclenche les Voters automatiquement. C'est la défense en profondeur : si un développeur oublie le filtre Doctrine sur une requête native, le Voter rattrape.
Chiffrement par tenant des données sensibles
Pour les données réglementées (santé, finance, RH), un chiffrement applicatif au niveau colonne est souvent exigé. Avec Doctrine, on utilise un type personnalisé qui chiffre/déchiffre à la volée avec une clé par tenant, stockée en vault. Sur la stratégie 1 (table unique), c'est même la seule manière d'obtenir une isolation cryptographique : un dump de la base ne donne rien sans la clé du tenant ciblé. Pour la conformité, voir la CNIL sur les bonnes pratiques de chiffrement.
Sauvegardes par tenant et droit à l'effacement RGPD
Le RGPD impose qu'un client puisse demander la suppression complète de ses données (article 17). Sur un SaaS multi-tenant, ça veut dire qu'on doit pouvoir extraire et purger un tenant sans toucher aux autres. Cela se prépare dès le modèle de données, pas au moment de la demande. Trois pratiques que j'applique : un script app:tenants:export qui produit un dump JSON ou CSV complet d'un tenant, un script app:tenants:purge qui supprime toutes les traces du tenant (y compris les logs, les fichiers uploadés et les audits), et une vérification automatique que le tenant_id est bien présent sur toutes les tables, y compris techniques.
Pour creuser la sécurité au-delà du multi-tenant, mon article sur l'OWASP Top 10 pour éditeurs SaaS détaille les dix catégories de vulnérabilités à couvrir systématiquement.
Scalabilité : de 10 à 10 000 tenants
Le multi-tenant n'a pas la même tête à 10 clients, à 1 000 clients et à 10 000 clients. Voici les seuils que j'observe en pratique, et les bascules à anticiper.
| Volume de tenants | Stratégie recommandée | Points d'attention |
|---|---|---|
| 1 à 50 tenants | Stratégie 1 (table unique) | Cadrage propre, monitoring basique, sauvegardes globales |
| 50 à 500 tenants | Stratégie 1 ou 2 selon le secteur | Index sur tenant_id, cache par tenant, monitoring par tenant |
| 500 à 5 000 tenants | Stratégie 1 avec sharding si besoin | Partitionnement SQL, files de jobs par tenant, queues séparées |
| 5 000 à 10 000+ tenants | Stratégie 1 obligatoire, sharding indispensable | Sharding horizontal, équipe ops dédiée, observabilité fine |
Le point qui surprend toujours les porteurs de projet : passer la stratégie 1 à grande échelle est mécaniquement plus simple que de gérer 5 000 schémas séparés (stratégie 2). L'isolation cryptographique par chiffrement de colonne et le sharding horizontal permettent de tenir des volumes très élevés sans changer de modèle. À l'inverse, si vous démarrez en stratégie 3 « pour être sûr » et que vous atteignez 200 clients, vous découvrez que la gestion opérationnelle devient un travail à temps plein.
Cas réel : un SaaS multi-tenant de facturation télécom
Sur un projet d'éditeur SaaS de facturation pour opérateurs télécom B2B, le porteur visait initialement 50 clients PME à 24 mois, et une centaine à 5 ans. Volumétrie modérée, contraintes RGPD standards (pas de données de santé), mais une exigence forte d'auditabilité par client. Nous avons retenu la stratégie 1 (table unique avec tenant_id), avec deux compléments : un chiffrement applicatif au niveau colonne sur les coordonnées bancaires des clients finaux, et un journal d'audit par tenant accessible en lecture seule via une interface dédiée.
Le projet est passé en production avec 5 clients pilotes en 14 semaines de développement. Trois ans plus tard, le SaaS sert une cinquantaine de clients pro, l'architecture n'a pas bougé, et le coût marginal d'un nouveau tenant tourne autour de quelques euros par mois en infrastructure. Aucune bascule n'a été nécessaire, et les audits RGPD régulièrement passés par les clients du SaaS sont validés sans réserve. La leçon : sur un SaaS B2B horizontal, la stratégie 1 bien implémentée tient largement la charge attendue.
Les erreurs fréquentes à éviter sur un SaaS multi-tenant PHP
Erreur 1 : oublier le filtrage sur une requête native
Le piège numéro un, c'est le développeur qui ajoute une requête SQL native dans le DBAL sans appliquer le filtre tenant_id. Un tenant voit alors les données d'un autre. La parade : interdire les requêtes natives sauf justification, et systématiser les tests d'intégration qui croisent deux tenants pour vérifier l'isolation. Sur les SaaS que je maintiens, ce test fait partie de la CI obligatoire.
Erreur 2 : sur-dimensionner dès le départ
Beaucoup de porteurs débutent avec une stratégie 3 « pour être tranquille ». Trois ans plus tard, ils ont 50 clients, une dette opérationnelle énorme et un coût d'infra disproportionné. La règle saine : commencer en stratégie 1, mesurer, et basculer seulement si une contrainte réelle l'impose. La migration de stratégie 1 vers 2 est faisable, l'inverse est presque impossible.
Erreur 3 : oublier le tenant_id sur les tables techniques
Les tables d'audit, de log applicatif, de fichiers uploadés ou de notifications doivent aussi porter le tenant_id. Sinon, on crée des fuites latérales : un export d'audit qui mélange les tenants, un fichier uploadé accessible par un autre client, une notification envoyée à mauvais escient. Ma règle : toute table créée dans le projet a un tenant_id, sauf justification écrite explicite (typiquement les tables de référentiel global, comme les codes pays).
Erreur 4 : laisser le tenant_id dans l'URL
Exposer le tenant_id dans l'URL (par exemple /tenant/42/facture/123) est un anti-pattern. Un utilisateur curieux peut modifier le 42 en 43 et tester si la requête passe. Mieux vaut résoudre le tenant côté serveur, depuis le token de sécurité ou le sous-domaine, et ne jamais lui faire confiance s'il vient du client. C'est l'OWASP A01 (Broken Access Control) appliqué au multi-tenant.
Cadrer votre projet de SaaS multi-tenant en PHP
Développer un SaaS multi-tenant en PHP n'est pas une question de framework ou de tool magique. C'est une question d'arbitrages structurants à poser au cadrage, à tenir dans la durée, et à challenger régulièrement en fonction de la croissance réelle du parc. La bonne stratégie, c'est celle qui correspond à votre secteur, votre volumétrie cible et vos contraintes réglementaires - pas la plus sophistiquée techniquement.
Si vous portez un projet de SaaS sur-mesure et que vous hésitez entre les trois stratégies d'isolation, je propose un atelier de cadrage qui examine votre cible, votre trajectoire et vos contraintes. Pour la trame de cadrage en amont, je détaille les sections à renseigner dans mon exemple de cahier des charges SaaS sur-mesure. Si votre projet part d'une application existante, ma page service sur la refonte SaaS legacy Symfony détaille la méthode progressive Strangler Pattern pour basculer un mono-tenant vers un multi-tenant sans interrompre la production.
Comme tous mes accompagnements, le périmètre est défini sur devis personnalisé après un échange initial gratuit de 30 minutes. Pour la méthodologie complète d'un projet SaaS sur-mesure, mon guide du développement de logiciel métier sur-mesure détaille les six étapes du cadrage à la maintenance long terme. Le formulaire de contact permet de prendre date.