skipToMainContentskipToFooter
NeuraAPI
NeuraAPI
Navigation
Architecture 10 septembre 2026 14 min de lecture

Architecture multi-tenant SaaS IA avec Next.js 14 : isolation, facturation et scale (2026)

Partager :

Vous lancez un SaaS IA mais chaque client a ses données, ses limites d'usage et sa facture ? Le multi-tenant n'est pas une option : c'est le cœur qui fait la différence entre un prototype et un produit facturable. Avec Next.js 14, Prisma et Stripe, vous pouvez poser une architecture propre dès le jour un — sans sur-ingénierie.

Dans ce guide, on détaille l'architecture qui fait tourner nos templates SaaS Next.js en production : un seul codebase, plusieurs milliers de tenants isolés, une facturation fine à l'usage IA et une scalabilité horizontale sans migration douloureuse.

Sommaire

  1. Multi-tenant : définition et modèles (shared DB vs schema vs DB)
  2. Modèle de données Prisma : tenantId partout, RLS light
  3. Middleware Next.js : résoudre le tenant à chaque requête
  4. Isolation forte : ce qu'il faut vraiment verrouiller
  5. Facturation par tenant avec Stripe : quotas IA et webhooks
  6. Rate limiting et quotas IA par tenant
  7. Observabilité et passage à l'échelle
  8. Checklist de mise en production

1. Quel modèle multi-tenant choisir ?

Trois approches dominent. Leur arbitrage est simple : coût d'opération contre isolation.

ModèleIsolationOpérationQuand l'adopter
Shared DB + tenantIdLogique (WHERE)Très simple95% des SaaS IA < 100k tenants
Schema par tenantForte (Postgres schema)Migrations N× schémasExigences réglementaires modérées
DB par tenantMaximaleLourde (backup, pool)Enterprise / données sensibles

Pour un SaaS IA qui démarre, le modèle shared DB avec colonne tenantId est le bon défaut : un seul schéma Prisma, des index composites, et une isolation garantie par le code (pas par l'infra). Vous pourrez migrer les gros clients vers un schéma dédié plus tard sans réécrire — uniquement en déplaçant leurs données.

Règle d'or : si vous hésitez entre deux modèles, prenez le plus simple et ajoutez des garde-fous logiciels. L'isolation parfaite ne sert à rien si elle vous empêche de shipper.

2. Modèle de données Prisma : tenantId partout

Chaque table métier porte tenantId. Pas d'exception. C'est ce qui rend l'isolation auditable et les requêtes prévisibles.

// prisma/schema.prisma
model Tenant {
  id        String   @id @default(cuid())
  slug      String   @unique
  name      String
  plan      String   @default("free") // free | pro | enterprise
  createdAt DateTime @default(now())
  users     User[]
  apiKeys   ApiKey[]
  usage     Usage[]
}

model User {
  id       String @id @default(cuid())
  tenantId String
  tenant   Tenant @relation(fields: [tenantId], references: [id])
  email    String @unique
  role     String @default("member")
  @@index([tenantId])
  @@unique([tenantId, email])
}

model Project {
  id       String @id @default(cuid())
  tenantId String
  tenant   Tenant @relation(fields: [tenantId], references: [id])
  name     String
  @@index([tenantId])
}

model Usage {
  id        String   @id @default(cuid())
  tenantId  String
  tenant    Tenant   @relation(fields: [tenantId], references: [id])
  tokensIn  Int      @default(0)
  tokensOut Int      @default(0)
  costCents Int      @default(0)
  createdAt DateTime @default(now())
  @@index([tenantId, createdAt])
}

Points clés : index composite [tenantId, createdAt] pour les requêtes de quota et de facturation, contrainte d'unicité par tenant (pas globale), et relation explicite vers Tenant. Toute requête métier passe par un helper qui injecte le tenant courant — jamais de findMany() sans filtre.

// src/lib/tenant.ts
import { prisma } from '@/lib/db'

export function tenantWhere(tenantId: string) {
  if (!tenantId) throw new Error('tenantId manquant — isolation violée')
  return { tenantId }
}

// Usage systématique
export const listProjects = (tenantId: string) =>
  prisma.project.findMany({ where: tenantWhere(tenantId) })

Pour aller plus loin sur les coûts par token, lisez notre analyse détaillée : Le coût réel d'une API IA en 2026 et Optimiser les coûts LLM en production.

3. Résoudre le tenant dans le middleware Next.js

Le tenant doit être résolu avant vos routes. Le middleware.ts tourne sur l'Edge : pas de Prisma, pas de Node crypto. On décode un cookie de session HMAC ou un header x-tenant-slug.

// src/middleware.ts (Edge — pas de Prisma)
import { NextResponse, type NextRequest } from 'next/server'

export function middleware(req: NextRequest) {
  const tenantSlug =
    req.headers.get('x-tenant-slug') ||
    req.nextUrl.hostname.split('.')[0] || // acme.app.com -> acme
    'default'

  // Propager au backend via header (validé côté API)
  const res = NextResponse.next()
  res.headers.set('x-tenant-slug', tenantSlug)
  return res
}

export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }

Côté API (Node.js), on valide le slug contre la base et on attache tenantId au contexte. Jamais faire confiance au header seul côté métier : toujours re-résoudre depuis une source authentifiée (session, API key).

// src/app/api/chat/route.ts (Node)
import { prisma } from '@/lib/db'
import { safeQuery } from '@/lib/db'

export async function POST(req: Request) {
  const apiKey = req.headers.get('x-api-key')
  const tenant = await safeQuery(
    () => prisma.apiKey.findUnique({ where: { key: apiKey! }, include: { tenant: true } }),
    null
  )
  if (!tenant) return new Response('Unauthorized', { status: 401 })

  // Toute la suite est scopée par tenant.tenant.id
  // ...
}

4. Isolation forte : la checklist qui évite les fuites

90% des fuites multi-tenant viennent d'un WHERE tenantId oublié. Voici les garde-fous concrets :

  • Helper obligatoire : interdisez les requêtes Prisma brutes sans tenantWhere() via une règle ESLint custom.
  • Tests d'isolation : un test par route qui tente de lire les données d'un autre tenant (doit renvoyer 404, pas 403 — ne pas révéler l'existence).
  • Recherche et cache : le cache (Redis/KV) est clé par tenantId:queryHash, jamais par queryHash seul. Idem pour les embeddings RAG.
  • Exports et webhooks : chaque export CSV et chaque webhook sortant est signé avec le tenantId dans le payload.
  • Admin global : si vous avez un panel admin, loguez chaque cross-tenant access (qui, quand, pourquoi).

Pour l'isolation RAG par tenant, notre tutoriel RAG en français avec Next.js et Groq et la comparaison RAG vs fine-tuning : décision d'architecture détaillent le pattern d'index vectoriel par tenant.

5. Facturation par tenant avec Stripe

Le multi-tenant et la facturation vont de pair. Chaque tenant a son stripeCustomerId et son stripeSubscriptionId. Le plan détermine les quotas IA, pas une variable d'env globale.

// Quotas par plan (source de vérité = base, pas le code)
const PLAN_QUOTAS = {
  free:       { tokensPerMonth: 100_000,  requestsPerMin: 10 },
  pro:        { tokensPerMonth: 5_000_000, requestsPerMin: 60 },
  enterprise: { tokensPerMonth: 50_000_000, requestsPerMin: 300 },
} as const

Les webhooks Stripe (customer.subscription.updated, invoice.paid) mettent à jour le champ Tenant.plan. La vérification de signature Stripe est idempotente (table webhookEvent). Pour l'implémentation complète, voir Webhooks Stripe avec Next.js : le tutoriel complet et Stripe Billing avec Next.js.

Facturation prête en 10 minutes

Nos templates SaaS Next.js incluent déjà Stripe multi-tenant, portail client et usage metering. Voir les prix — pas de surprise, code source inclus.

6. Rate limiting et quotas IA par tenant

Sans garde-fou, un seul tenant peut brûler votre budget Groq/OpenAI en quelques heures. Deux niveaux de protection :

  • Rate limiting Edge : 100 req/min par IP + requestsPerMin par tenant (Map en mémoire sur l'Edge, UPSERT atomique en base côté API).
  • Quota mensuel : compteur de tokens par tenant, vérifié avant chaque appel IA. En cas de dépassement, réponse 429 avec header Retry-After et redirection vers /pricing.
// src/lib/rate-limit.ts — vérif avant chaque call IA
export async function checkTenantQuota(tenantId: string) {
  const plan = await getTenantPlan(tenantId)
  const quota = PLAN_QUOTAS[plan]
  const used = await getMonthlyUsage(tenantId) // SUM(tokensIn+tokensOut)
  if (used >= quota.tokensPerMonth) {
    throw Object.assign(new Error('Quota dépassé'), { status: 429 })
  }
}

Complétez avec un circuit breaker sur vos providers IA pour éviter les cascades de panne quand un tenant déclenche une rafale, et consultez Monitoring IA en production pour les métriques par tenant.

7. Observabilité et passage à l'échelle

Le jour où un tenant pèse 30% de votre trafic, vous devez le voir avant qu'il ne vous réveille à 3h du matin.

  • Logs structurés : chaque log porte tenantId et requestId. Filtrage instantané par client.
  • Métriques par tenant : latence p95, tokens/min, erreurs IA — exposées par tenant dans votre dashboard.
  • Isolation du noisy neighbor : file d'attente IA séparée par tenant (ou au moins par plan) pour que le free tier ne ralentisse pas l'enterprise.
  • Sharding futur : gardez tenantId comme clé de sharding. Quand Postgres force la main, vous pourrez déplacer un gros tenant vers son propre schéma sans changer le code applicatif.

Côté Next.js, ne cassez pas le SSG : gardez les pages marketing en statique et isolez les routes tenant-dépendantes en dynamique. Notre guide Edge Runtime pour l'IA et Patterns Edge Functions IA couvrent ces arbitrages.

8. Checklist de mise en production

  • Toute table métier a tenantId + index composite
  • Middleware résout le tenant, API re-valide — jamais confiance au header seul
  • Helper tenantWhere() obligatoire (ESLint) + tests d'isolation cross-tenant
  • Cache et embeddings cléés par tenantId
  • Stripe webhook idempotent + quotas par plan en base
  • Rate limiting Edge + quota mensuel avant chaque call IA
  • Logs et métriques taggés tenantId, dashboard par tenant
  • RGPD : suppression par tenant en une requête (cascade), voir IA & RGPD 2026

Démarrez avec une base multi-tenant propre

Plutôt que de bricoler l'isolation après coup, partez d'une base qui l'intègre nativement. Explorer les templates ou consulter l'API docs pour voir le pattern en action.

Voir les prix →

Conclusion

Une architecture multi-tenant bien pensée ne se voit pas — elle se ressent : pas de fuite de données, pas de facture surprise, pas de nuit blanche quand un client scale. Avec Next.js 14, un tenantId systématique, un middleware Edge léger et une facturation Stripe par tenant, vous avez une fondation qui tient de 10 à 10 000 clients sans réécrire. Nos templates intègrent déjà ce pattern complet — concentrez-vous sur votre valeur IA, pas sur la plomberie multi-tenant.