Documentation

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: boolean

Quand 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
  • i18npickLocalized et conventions

On this page