Feature flags
Activation progressive de features par toggle global, % de rollout déterministe ou allowlist d'users.
Système de feature flags simple, en DB, sans dépendance externe (GrowthBook, LaunchDarkly…). 3 leviers combinables par flag.
Les 3 leviers
| Levier | Effet |
|---|---|
enabled: boolean | Toggle global on/off |
rolloutPct: 0-100 | % d'users qui voient la feature, déterministe par userId |
userIds: string[] | Allowlist explicite — toujours actif même si enabled=false |
Le bucketing du rolloutPct utilise sha256(key + userId) % 100 → un user
voit toujours le même résultat (pas de flicker entre requêtes), et à
25 % ce sont toujours les mêmes 25 %.
Cas d'usage typiques
| Besoin | Config |
|---|---|
| Activer pour tous | enabled=true, rolloutPct=100 |
| Toi seul (dev) | enabled=false, userIds=["cuid_du_user"] |
| 10 % des users | enabled=true, rolloutPct=10 |
| Équipe interne + 25 % public | enabled=true, rolloutPct=25, userIds=[...team] |
| Kill-switch | enabled=false, userIds=[] |
Utilisation côté code
import { isFeatureEnabled } from "@/lib/feature-flags";
const session = await auth();
const showNewDashboard = await isFeatureEnabled("new-dashboard", session?.user?.id);
if (showNewDashboard) return <NewDashboard />;
return <OldDashboard />;Le helper est cached via unstable_cache (60 s + tag feature-flags).
Les routes admin appellent revalidateTag("feature-flags") à chaque
modification.
Anonyme (userId = null) : seul enabled=true && rolloutPct=100 les active.
Logique car on ne peut pas bucket un user inconnu.
Admin — créer / modifier un flag
Page /admin/feature-flags :
- Bouton Nouveau flag → modal (key + description)
- Sur chaque flag : toggle
enabled, slider rollout %, suppression - (Allowlist
userIdséditable via API directement, pas encore en UI)
Routes API
GET /api/admin/feature-flags → liste tous les flags
POST /api/admin/feature-flags → create { key, description?, enabled, rolloutPct, userIds }
PATCH /api/admin/feature-flags/[id] → update partiel
DELETE /api/admin/feature-flags/[id]Toutes admin-only (requireApiAuth({ admin: true })) avec audit log
(feature_flag.create / update / delete).
Modèle Prisma
model FeatureFlag {
id String @id @default(cuid())
key String @unique
description String?
enabled Boolean @default(false)
rolloutPct Int @default(0)
userIds String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Scaling
Le cache est mémoire-process. En multi-instance (Vercel serverless),
chaque instance a son propre cache de 60 s — un toggle peut prendre
jusqu'à 60 s à se propager partout. Pour un kill-switch instantané,
appelle revalidateTag("feature-flags") partout (ou passe à Redis).