Plans tarifaires
Modèle Plan, multi-langue, lien Stripe et page admin.
Les plans sont stockés en DB (pas dans le code). Ça veut dire que tu peux modifier prix, features et CTA depuis l'admin sans redéploiement.
Modèle Prisma
model Plan {
id String @id @default(cuid())
key String @unique
name Json // LocalizedText
tagline Json // LocalizedText
features Json // { fr: string[], en: string[] }
cta Json // LocalizedText
priceMonthly Int // en cents
priceYearly Int
currency String @default("EUR")
stripePriceIdMonthly String?
stripePriceIdYearly String?
stripeProductId String?
highlighted Boolean @default(false)
active Boolean @default(true)
displayOrder Int @default(0)
createdAt DateTime @default(now())
}Multi-langue
name, tagline, cta sont des LocalizedText (Record<string, string>),
features est Record<string, string[]>. Helper pickLocalized() choisit
la bonne langue avec fallback locale → fr → en → première dispo :
import { pickLocalized } from "@/lib/site-config";
const name = pickLocalized(plan.name, "en");Ou tu utilises localizePlan(plan, locale) qui te donne un objet aplati,
prêt à rendre.
Récupérer les plans
import { getActivePlans, getAllPlans, getPlan } from "@/lib/plans";
// Public — actifs uniquement, cache 5 min, tag "plans"
const plans = await getActivePlans();
// Admin — tout, ordonné (active first)
const allPlans = await getAllPlans();revalidateTag("plans") est appelé après chaque mutation admin pour purger
le cache.
Lier à Stripe
Tu crées les Products + Prices dans Stripe Dashboard, puis tu copies les
Price IDs (price_xxx) dans le formulaire admin du plan (champs
stripePriceIdMonthly et stripePriceIdYearly). La route
/api/billing/checkout utilise ces IDs pour générer la session.
Si un plan n'a pas de Price ID, son bouton de souscription est désactivé.
Page admin /plans
Route : app/[locale]/(dashboard)/plans/page.tsx. CRUD complet, avec :
- Bascule active/inactive (sans suppression destructive)
- Édition multi-langue inline
- Réordonnancement par
displayOrder - Highlight pour mettre un plan en avant (badge "populaire")
API derrière : /api/admin/plans et /api/admin/plans/[id]. Toutes les
mutations sont auditées (plan.create, plan.update, plan.delete).
Mode billing on/off
Le boilerplate ship avec un kill-switch global :
SiteConfig.billingEnabled: booleanQuand false, toute l'UI billing (page pricing, bouton subscribe, onglet
Billing dans /account/settings, page admin Plans) est masquée. Utile en
dev ou si tu lances sans monétisation au départ.
Allez plus loin
- Coupons — codes promo et restrictions plans
- Webhooks Stripe — synchronisation subscriptions
- i18n —
pickLocalizedet conventions