Que fait saas ?
L'état de l'art SaaS — multi-tenancy, facturation à l'usage, PLG, intégration de fonctionnalités IA, observabilité moderne, conformité et sécurité pour les produits SaaS en production.
Instructions
Multi-tenancy
Choisissez le modèle d'isolation adapté à votre cas d'usage :
| Modèle | Description | Quand l'utiliser |
|---|---|---|
| Schéma partagé | Tous les tenants partagent les tables ; les lignes sont marquées avec tenant_id | Stade précoce, sensible aux coûts, conformité simple |
| Schéma par tenant | Schéma de base de données séparé par tenant | Isolation moyenne, migrations faciles par tenant |
| Base par tenant | Base de données séparée par tenant | Entreprise, résidence stricte des données, conformité élevée (SOC 2, HIPAA) |
| Hybride | Données centrales partagées ; données sensibles isolées | Secteurs réglementés avec offres multi-niveaux |
Règles pour la location de schéma partagé :
- Chaque requête doit filtrer par
tenant_id— appliquez via un dépôt de base ou un middleware ORM, pas par la discipline du développeur - Utilisez la Sécurité au niveau des lignes (RLS) de PostgreSQL comme deuxième couche de défense :
ALTER TABLE orders ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.current_tenant')::uuid); - Indexez
tenant_idsur chaque table à portée de tenant - Écrivez des tests d'intégration qui vérifient l'isolation des données entre tenants
- Limitez tous les chemins de stockage d'objets :
s3://bucket/{tenantId}/uploads/{fileId} - N'exposez jamais d'ID séquentiels prévisibles — utilisez des UUID (v7 pour la triabilité)
Abonnements & Facturation
Abonnements à tarif fixe
- Déléguez la facturation à un fournisseur (Stripe, Paddle, LemonSqueezy) — n'implémentez jamais le traitement des paiements en interne
- Stockez uniquement
customerIdetsubscriptionIddans votre base de données ; jamais les données de carte - Synchronisez l'état via des webhooks — traitez votre base de données comme une réplique en lecture cohérente en dernier ressort de la vérité du fournisseur de facturation :
customer.subscription.created → activer le plan customer.subscription.updated → mise à niveau / rétrogradation / changement de siège customer.subscription.deleted → annuler + commencer la période de grâce invoice.payment_failed → dunning : notifier → restreindre → annuler - Utilisez l'
idde l'événement webhook comme clé d'idempotence — stockez-la avant le traitement ; ignorez si déjà vue - Stockez les payloads bruts des webhooks pour l'audit et la relecture
Facturation à l'usage / mesurée (par défaut moderne)
- Suivez les événements d'utilisation au fur et à mesure (appels API, sièges, jetons consommés, Go de stockage) :
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, { quantity: tokensUsed, timestamp: Math.floor(Date.now() / 1000), action: 'increment', }); - Exposez un tableau de bord d'utilisation en direct aux clients — les surprises le jour de la facturation provoquent le désabonnement
- Implémentez des limites souples (avertir à 80 %) et des limites strictes (bloquer à 100 %) par période de facturation
- Envisagez un niveau gratuit + une vente incitative basée sur l'utilisation comme rampe d'accès à la Croissance pilotée par le produit (PLG)
Croissance pilotée par le produit (PLG)
- Niveau gratuit / freemium — laissez les utilisateurs expérimenter la valeur avant de demander un paiement ; fixez des limites qui rendent les offres payantes manifestement intéressantes
- Délai de valorisation (TTV) — mesurez et minimisez le temps entre l'inscription et la première action significative ("moment aha")
- Invitations à la mise à niveau dans l'application — affichez des appels à l'action de mise à niveau de manière contextuelle au moment où une limite du plan est atteinte, pas dans une page de paramètres générique
- Leads qualifiés par le produit (PQL) — notez les utilisateurs en fonction des signaux d'utilisation du produit (pas seulement des demandes de démo) pour les orienter vers les ventes
- Fonctionnalités virales / collaboratives — invitez des coéquipiers, partagez des rapports, des pages publiques — chaque action est un canal de distribution
Intégration de fonctionnalités IA
Les produits SaaS modernes sont censés proposer des capacités natives d'IA. Appliquez ces modèles :
Assistants IA et copilotes :
- Utilisez des réponses en streaming (SSE /
ReadableStream) afin que les utilisateurs voient les jetons arriver — ne leur faites jamais attendre une réponse complète - Rendez toujours la sortie de l'IA modifiable — les utilisateurs doivent pouvoir corriger, affiner et s'approprier le résultat
- Affichez des signaux de confiance et citez les sources (RAG) pour instaurer la confiance
Génération augmentée par récupération (RAG) :
- Stockez les embeddings vectoriels par tenant dans un espace de noms isolé (Pinecone, pgvector, Qdrant) — ne mélangez jamais les données des tenants
- Intégrez le contenu lors de l'écriture ; interrogez lors de la lecture avec similarité cosinus
- Découpez les documents intelligemment (512–1024 jetons avec chevauchement) ; réintégrez lorsque le contenu source change
Contrôles d'utilisation et des coûts :
- Suivez la consommation de jetons LLM par tenant — facturez ou limitez le débit en conséquence
- Mettez en cache les prompts déterministes (même entrée → même sortie) pour réduire les coûts
- Utilisez des modèles moins chers pour la classification/le triage ; réservez les modèles de pointe pour les tâches de génération
Confidentialité des données :
- Proposez un avenant de traitement des données (DPA) qui indique explicitement si les données des locataires sont utilisées pour la formation du modèle
- Offrez une option de retrait ; les clients Enterprise l'exigeront
Feature Flags & Gestion des fonctionnalités par plan
- Limitez l'accès aux fonctionnalités au niveau du service — le masquage côté client est une question d'apparence, pas de sécurité
- Centralisez la logique des droits dans un helper unique
can(tenant, feature):function can(tenant: Tenant, feature: Feature): boolean { return tenant.plan.features.includes(feature); } - Dissociez le déploiement de la fonctionnalité de sa mise à disposition à l'aide d'un système de flags (LaunchDarkly, Unleash, GrowthBook ou une simple table dans la base de données)
- Utilisez les flags pour : déploiements progressifs (% de tenants), programmes bêta, tests A/B, interrupteurs d'arrêt d'urgence
Intégration
- Minimisez le TTV : guidez les utilisateurs vers leur première action significative en moins de 5 minutes
- Utilisez un flux d'intégration basé sur une liste de tâches avec des indicateurs de progression clairs
- Pré-remplissez des exemples / données de démonstration pour que le produit ne semble pas vide lors de la première utilisation
- Collectez le minimum de données absolu dès le départ ; reportez tout le reste
- Déclenchez les e-mails d'intégration en fonction d'événements produit (pas seulement du temps) : bienvenue lors de l'inscription, rappel s'ils n'ont pas terminé l'étape 2 après 24h, félicitations pour le premier jalon
- Proposez une visite interactive du produit (pas une vidéo) afin que les utilisateurs apprennent en faisant
Observabilité & Opérations
Journalisation structurée — chaque ligne de journal doit inclure :
{ "level": "info", "msg": "...", "tenantId": "...", "userId": "...", "requestId": "...", "timestamp": "..." }
Traçage distribué avec OpenTelemetry :
- Instrumentez au niveau du framework (Next.js
instrumentation.ts, middleware Express) - Propagez le contexte de trace entre les limites des services (en-tête
traceparent) - Exportez vers votre backend de choix (Jaeger, Honeycomb, Datadog, Grafana Tempo)
Indicateurs par tenant à suivre :
- Utilisateurs actifs (DAU/MAU), volume d'appels API, taux d'erreur, latence p50/p95/p99, adoption des fonctionnalités, stockage consommé
Alerting :
- Alertez sur les anomalies au niveau du tenant — pic d'erreur soudain, baisse d'utilisation, échecs de facturation
- Définissez des SLO par niveau : gratuit (au mieux), pro (99,5 %), enterprise (99,9 %)
Outils d'administration (internes) :
- Usurpez l'identité d'un tenant pour le support (auditez chaque usurpation)
- Consultez le journal d'audit, gérez les abonnements, remplacez les feature flags, déclenchez une ré-intégration
- Construisez un tableau de bord d'état : statut par tenant, erreurs récentes, tendances d'utilisation
Données, Analytique & Conformité
Suivi d'événements :
- Émettez des événements produit structurés (
user.signed_up,feature.used,subscription.upgraded) vers un entrepôt de données (Snowflake, BigQuery) ou un CDP (Segment, PostHog, RudderStack) - Utilisez ces événements pour la prédiction de désabonnement, l'analyse de cohorte et la notation PLG
RGPD / confidentialité :
- Fournissez un point de terminaison d'exportation de données et un point de terminaison de suppression de compte — requis par le RGPD et attendu par les acheteurs enterprise
- Documentez votre politique de conservation des données ; mettez en œuvre des purges automatisées
- Utilisez la gestion du consentement pour les cookies marketing et l'analyse
SOC 2 / Hygiène de conformité (prêt pour l'enterprise) :
- Maintenez un journal d'audit de toutes les actions utilisateur modifiant l'état (qui, quoi, quand, depuis quelle IP)
- Implémentez un contrôle d'accès basé sur les rôles (RBAC) avec des privilèges minimaux par défaut
- Appliquez le MFA pour tous les comptes administrateur ; proposez le SSO (SAML/OIDC) pour les clients enterprise
- Effectuez régulièrement des analyses de vulnérabilité des dépendances et des tests d'intrusion
- Maintenez une politique de divulgation des vulnérabilités
Isolation & Sécurité des Données
- Validez
tenant_idsur chaque appel API par rapport à la session authentifiée — ne faites jamais confiance au corps de la requête - Limitez le débit par tenant pour éviter les effets de voisinage bruyant
- Hachez les clés API dans la base de données (bcrypt ou Argon2) ; ne stockez jamais en texte clair
- Exigez HTTPS ; définissez
Strict-Transport-Securityavec unmax-agelong - Utilisez
HttpOnly ; Secure ; SameSite=Laxpour les cookies de session
Exemples
// Dépôt à portée de tenant avec contexte RLS
class TenantRepository<T> {
constructor(
private readonly tenantId: string,
private readonly db: PrismaClient,
) {}
protected async withTenantContext<R>(fn: () => Promise<R>): Promise<R> {
return this.db.$transaction(async (tx) => {
await tx.$executeRaw`SELECT set_config('app.current_tenant', ${this.tenantId}, true)`;
return fn();
});
}
async findById(id: string): Promise<T | null> {
return this.withTenantContext(() =>
(this.db as any).findFirst({ where: { id, tenantId: this.tenantId } })
);
}
}
// Point de terminaison de streaming IA avec comptage d'utilisation par tenant
export async function POST(req: Request) {
const session = await getSession(req);
const { prompt } = await req.json();
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
let tokensUsed = 0;
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content ?? '';
controller.enqueue(new TextEncoder().encode(text));
tokensUsed += chunk.usage?.completion_tokens ?? 0;
}
controller.close();
// mesurer l'utilisation après la fin du streaming
await meteringService.record(session.tenantId, 'ai_tokens', tokensUsed);
},
});
return new Response(readable, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
// Webhook Stripe avec idempotence
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, req.headers['stripe-signature']!, process.env.STRIPE_WEBHOOK_SECRET!
);
const seen = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
if (seen) return res.sendStatus(200);
await db.webhookEvent.create({ data: { stripeEventId: event.id, payload: event } });
switch (event.type) {
case 'customer.subscription.updated':
await subscriptionService.sync(event.data.object);
break;
case 'customer.subscription.deleted':
await subscriptionService.cancel(event.data.object.metadata.tenantId);
break;
}
res.sendStatus(200);
});