Next.js SaaS
Concevoir un SaaS multitenant dans Next.js : isolation, routage et propriété des données
Concevez la résolution des tenants, la propriété des données, les autorisations, les clés de cache, les tâches, les domaines personnalisés et le déploiement d’un SaaS Next.js multitenant.
L’architecture multitenant impose un invariant : chaque lecture, écriture, entrée de cache, tâche, journal et nom d’hôte doit être associé au bon tenant avant tout travail privilégié.
Définir ce qu’est un tenant
Un tenant peut être une organisation, un espace de travail, une boutique ou un compte client. Définissez qui le crée, qui en fait partie, si un utilisateur peut en rejoindre plusieurs et quelles données lui appartiennent. Utilisez un identifiant de tenant stable en interne, même si son identifiant public est un slug ou un nom d’hôte personnalisé.
Décrivez la garantie d’isolation. Une infrastructure partagée avec propriété au niveau des lignes diffère d’une base par tenant. Le modèle plus fort coûte davantage à provisionner et exploiter, mais peut être justifié par la réglementation, l’échelle ou les besoins clients. Choisissez selon la limite réelle du produit plutôt qu’un idéal abstrait.
Résoudre le contexte du tenant depuis des données fiables
Les applications fondées sur le chemin peuvent résoudre /acme/projects depuis le slug de route. Les applications à sous-domaines ou domaines personnalisés associent l’hôte demandé à un tenant. Normalisez l’hôte, rejetez les inconnus et ne faites jamais confiance à un identifiant de tenant fourni par formulaire si la route authentifiée établit déjà le contexte.
Proxy peut effectuer tôt des réécritures d’hôte ou du routage optimiste, mais le code serveur doit aussi assurer une résolution sûre. Transmettez un contexte de tenant vérifié à la couche de données ; ne laissez pas chaque composant analyser les en-têtes et deviner la propriété indépendamment.
export async function resolveTenant(host: string) {
const normalized = host.toLowerCase().split(":")[0];
const tenant = await db.tenant.findUnique({
where: { hostname: normalized },
});
if (!tenant) notFound();
return tenant;
}Vérifier la propriété dans chaque requête
Ajoutez tenantId aux enregistrements des tables partagées et incluez-le dans chaque recherche, mise à jour, suppression et contrainte d’unicité. Un identifiant globalement unique ne constitue pas un contrôle d’autorisation. Interrogez selon l’identifiant demandé et celui du tenant vérifié pour qu’un identifiant divulgué ne franchisse pas la limite.
La sécurité des lignes en base peut apporter une défense en profondeur si la stack la prend en charge, mais l’autorisation applicative reste nécessaire. Testez volontairement les refus en créant deux tenants et en tentant chaque opération protégée avec les identifiants de l’autre.
const project = await db.project.findFirst({
where: {
id: projectId,
tenantId: context.tenantId,
},
});Limiter les rôles à chaque tenant
Un utilisateur peut être owner dans un tenant et viewer dans un autre. Stockez les rôles sur les appartenances, pas dans un champ global de l’utilisateur. Résolvez le tenant puis l’appartenance, et vérifiez la permission exacte exigée par l’opération.
Évitez de disperser les chaînes de rôles entre les composants. Centralisez les règles de permission dans des fonctions qui renvoient des décisions métier et répétez le contrôle dans les Server Actions et Route Handlers. L’interface peut masquer les commandes indisponibles pour la clarté ; l’autorisation serveur protège l’opération.
Séparer les caches, tâches et stockage
Chaque clé de cache partagé doit inclure l’identité du tenant. Une clé projects peut exposer les résultats d’un tenant à un autre ; projects:tenant-id établit la limite. Appliquez la même règle aux tags de cache, limites de débit, chemins de stockage objet, index de recherche, données des tâches et clés d’idempotence.
Les workers d’arrière-plan doivent revérifier l’autorisation ou utiliser un contexte de tenant fiable et immuable enregistré à la création de la tâche. Incluez des identifiants adaptés à l’isolation des tenants dans les journaux, sans journaliser le contenu privé des clients pour faciliter le débogage.
"use cache";
cacheTag("projects:" + tenantId);
return db.project.findMany({ where: { tenantId } });Traiter les domaines personnalisés comme une configuration vérifiée
Un tenant ne doit pas pouvoir revendiquer un nom d’hôte en le saisissant dans un formulaire. Exigez une vérification avant de router le trafic, empêchez l’attribution d’un hôte à deux tenants et définissez ce qui se passe en cas de changement de propriétaire. Gardez les domaines propres à la plateforme distincts de ceux gérés par les clients.
Générez les URL canoniques depuis le nom d’hôte vérifié du tenant si ses pages publiques sont indexables. Les tableaux de bord authentifiés ne doivent normalement pas être indexés. Adaptez redirections et cookies au modèle d’hôtes, surtout si les utilisateurs passent d’un domaine de connexion central à celui d’un tenant.
Tester l’isolation comme propriété du système
Créez des jeux de données automatisés avec au moins deux tenants et deux rôles. Testez les pages directes, Server Actions, Route Handlers, exports, caches, tâches, recherches et résolutions d’hôtes. Masquer seulement un élément de navigation d’un autre tenant ne teste pas l’isolation.
Examinez les requêtes et journaux pour repérer les conditions de tenant manquantes. Incluez tenantId dans les contraintes d’unicité si les noms ne doivent être uniques qu’à l’intérieur d’un tenant. Testez la suppression et l’export du tenant pour que les données d’arrière-plan ne subsistent pas accidentellement sans propriétaire.
Déployer le routage des tenants avec des versions observables
Adios relie à la version applicative le routage Next.js, l’environnement persistant, la dépendance de base, les références aux secrets, les journaux et les domaines personnalisés. Testez dans l’aperçu un hôte de la plateforme, un hôte de tenant vérifié, un hôte inconnu, deux comptes de tenants et le contrôle de santé avant la promotion.
Les journaux de compilation et d’exécution distinguent une version échouée d’une erreur de données propre à un tenant. Le TLS géré et la promotion gardent les routes publiques vérifiées sur la version opérationnelle, avec des changements de source et de manifeste vérifiables. L’isolation des tenants relève toujours de l’application et des données ; l’hébergement rend cette conception déployable et inspectable.
name: multi-tenant-app
build_cmd: npm ci && npm run build
start_cmd: npm start
runtime:
name: node@24
port: 3000
health_path: /api/health
env:
DATABASE_URL: secret://DATABASE_URL