Construire un Starter Kit SaaS avec Next.js 15, Stripe et Auth.js v5

Construisez votre propre systeme de facturation SaaS de zero. Ce tutoriel vous guide pas a pas dans la creation d'une application d'abonnement complete avec Next.js 15 Server Actions, Stripe Checkout, le portail client et Auth.js v5 — tout ce dont vous avez besoin pour lancer un vrai produit SaaS.
Ce que vous allez apprendre
A la fin de ce tutoriel, vous saurez :
- Configurer Auth.js v5 avec les fournisseurs GitHub et Google OAuth
- Integrer Stripe Checkout pour la facturation par abonnement
- Construire une page de tarification avec plusieurs niveaux de plan
- Gerer les webhooks Stripe pour synchroniser l'etat des abonnements
- Creer des routes protegees avec middleware et verification de session
- Implementer un portail client pour la gestion des abonnements
- Stocker les donnees utilisateurs et abonnements avec Drizzle ORM et PostgreSQL
Prerequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installe
- Un compte Stripe (gratuit sur stripe.com)
- Une application GitHub OAuth ou un projet Google Cloud pour l'authentification
- PostgreSQL en local (ou une instance hebergee comme Neon ou Supabase)
- Une connaissance de base de Next.js et TypeScript
Vue d'ensemble de l'architecture
┌─────────────────────────────────────────────┐
│ Next.js 15 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Auth.js │ │ Stripe │ │ Drizzle │ │
│ │ v5 │ │ Checkout │ │ ORM │ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼──────────────▼─────┐ │
│ │ Server Actions │ │
│ └────────────────────────────────────────┘ │
└──────────────────┬───────────────────────────┘
│
┌─────────▼─────────┐
│ PostgreSQL │
│ (Users, Subs) │
└───────────────────┘
Etape 1 : Creer le projet Next.js
Commencez par creer une nouvelle application Next.js 15 avec TypeScript et App Router :
npx create-next-app@latest saas-starter --typescript --tailwind --eslint --app --src-dir
cd saas-starterInstallez les dependances necessaires :
npm install next-auth@beta @auth/drizzle-adapter drizzle-orm @neondatabase/serverless stripe
npm install -D drizzle-kit dotenvCreez votre fichier .env.local avec les variables d'environnement requises :
# Base de donnees
DATABASE_URL="postgresql://user:password@localhost:5432/saas_starter"
# Auth.js
AUTH_SECRET="run-npx-auth-secret-to-generate"
AUTH_GITHUB_ID="your-github-client-id"
AUTH_GITHUB_SECRET="your-github-client-secret"
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
# Stripe
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Application
NEXT_PUBLIC_APP_URL="http://localhost:3000"Generez votre AUTH_SECRET en executant npx auth secret dans votre terminal. Ne commitez jamais cette valeur dans le controle de version.
Etape 2 : Configurer le schema de base de donnees avec Drizzle ORM
Creez le schema de base de donnees qui gere a la fois les sessions Auth.js et les abonnements Stripe.
Creez src/db/schema.ts :
import {
pgTable,
text,
timestamp,
integer,
primaryKey,
boolean,
} from "drizzle-orm/pg-core";
import type { AdapterAccountType } from "next-auth/adapters";
// Tables requises par Auth.js
export const users = pgTable("users", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
stripeCustomerId: text("stripe_customer_id").unique(),
stripePriceId: text("stripe_price_id"),
stripeSubscriptionId: text("stripe_subscription_id").unique(),
stripeSubscriptionStatus: text("stripe_subscription_status"),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow(),
});
export const accounts = pgTable(
"accounts",
{
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccountType>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => [
primaryKey({
columns: [account.provider, account.providerAccountId],
}),
]
);
export const sessions = pgTable("sessions", {
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
});
export const verificationTokens = pgTable(
"verificationTokens",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(verificationToken) => [
primaryKey({
columns: [verificationToken.identifier, verificationToken.token],
}),
]
);Creez src/db/index.ts pour la connexion a la base de donnees :
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });Creez drizzle.config.ts a la racine du projet :
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Executez la migration :
npx drizzle-kit pushEtape 3 : Configurer Auth.js v5
Creez src/auth.ts :
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [GitHub, Google],
callbacks: {
session({ session, user }) {
session.user.id = user.id;
return session;
},
},
pages: {
signIn: "/login",
},
});Creez la route API dans src/app/api/auth/[...nextauth]/route.ts :
import { handlers } from "@/auth";
export const { GET, POST } = handlers;Creez la page de connexion dans src/app/login/page.tsx :
import { signIn } from "@/auth";
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-sm space-y-6 rounded-xl border p-8 shadow-sm">
<div className="text-center">
<h1 className="text-2xl font-bold">Bon retour</h1>
<p className="mt-2 text-sm text-gray-500">
Connectez-vous a votre compte pour continuer
</p>
</div>
<form
action={async () => {
"use server";
await signIn("github", { redirectTo: "/dashboard" });
}}
>
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800"
>
Continuer avec GitHub
</button>
</form>
<form
action={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
>
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium hover:bg-gray-50"
>
Continuer avec Google
</button>
</form>
</div>
</div>
);
}Etape 4 : Proteger les routes avec Middleware
Creez src/middleware.ts pour proteger les routes authentifiees :
import { auth } from "@/auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");
const isOnLogin = req.nextUrl.pathname === "/login";
if (isOnDashboard && !isLoggedIn) {
return Response.redirect(new URL("/login", req.nextUrl));
}
if (isOnLogin && isLoggedIn) {
return Response.redirect(new URL("/dashboard", req.nextUrl));
}
});
export const config = {
matcher: ["/dashboard/:path*", "/login"],
};Etape 5 : Configurer les produits et tarifs Stripe
Avant d'ecrire le code, creez vos produits dans le tableau de bord Stripe ou utilisez le CLI Stripe :
# Installer Stripe CLI
brew install stripe/stripe-cli/stripe
# Se connecter a votre compte Stripe
stripe login
# Creer les produits et les tarifs
stripe products create --name="Starter" --description="Pour les particuliers et petits projets"
stripe prices create --product=prod_xxx --unit-amount=900 --currency=usd --recurring[interval]=month
stripe products create --name="Pro" --description="Pour les equipes et entreprises en croissance"
stripe prices create --product=prod_yyy --unit-amount=2900 --currency=usd --recurring[interval]=month
stripe products create --name="Enterprise" --description="Pour les grandes organisations"
stripe prices create --product=prod_zzz --unit-amount=9900 --currency=usd --recurring[interval]=monthCreez src/lib/stripe.ts :
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-12-18.acacia",
typescript: true,
});
export const PLANS = [
{
name: "Starter",
description: "Pour les particuliers et petits projets",
price: "9 $",
priceId: process.env.STRIPE_STARTER_PRICE_ID!,
features: [
"Jusqu'a 3 projets",
"Analytique de base",
"Support par email",
"1 Go de stockage",
],
},
{
name: "Pro",
description: "Pour les equipes en croissance",
price: "29 $",
priceId: process.env.STRIPE_PRO_PRICE_ID!,
popular: true,
features: [
"Projets illimites",
"Analytique avancee",
"Support prioritaire",
"10 Go de stockage",
"Collaboration d'equipe",
"Integrations personnalisees",
],
},
{
name: "Enterprise",
description: "Pour les grandes organisations",
price: "99 $",
priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
features: [
"Tout dans Pro",
"Stockage illimite",
"Support dedie",
"SSO et SAML",
"Contrats personnalises",
"Garanties SLA",
],
},
] as const;Etape 6 : Construire la page de tarification
Creez src/app/pricing/page.tsx :
import { auth } from "@/auth";
import { PLANS } from "@/lib/stripe";
import { CheckoutButton } from "./checkout-button";
export default async function PricingPage() {
const session = await auth();
return (
<div className="mx-auto max-w-5xl px-4 py-16">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight">
Tarification simple et transparente
</h1>
<p className="mt-4 text-lg text-gray-500">
Choisissez le plan qui correspond a vos besoins. Changez de plan a
tout moment.
</p>
</div>
<div className="mt-16 grid gap-8 md:grid-cols-3">
{PLANS.map((plan) => (
<div
key={plan.name}
className={`relative rounded-2xl border p-8 shadow-sm ${
plan.popular
? "border-blue-600 ring-2 ring-blue-600"
: "border-gray-200"
}`}
>
{plan.popular && (
<span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-blue-600 px-3 py-1 text-xs font-semibold text-white">
Le plus populaire
</span>
)}
<h3 className="text-lg font-semibold">{plan.name}</h3>
<p className="mt-1 text-sm text-gray-500">{plan.description}</p>
<p className="mt-6">
<span className="text-4xl font-bold">{plan.price}</span>
<span className="text-gray-500">/mois</span>
</p>
<CheckoutButton
priceId={plan.priceId}
isLoggedIn={!!session}
popular={plan.popular}
/>
<ul className="mt-8 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm">
<svg
className="h-4 w-4 text-green-500"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
{feature}
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}Creez le bouton de paiement en tant que composant client dans src/app/pricing/checkout-button.tsx :
"use client";
import { useTransition } from "react";
import { createCheckoutSession } from "./actions";
interface CheckoutButtonProps {
priceId: string;
isLoggedIn: boolean;
popular?: boolean;
}
export function CheckoutButton({
priceId,
isLoggedIn,
popular,
}: CheckoutButtonProps) {
const [isPending, startTransition] = useTransition();
const handleCheckout = () => {
startTransition(async () => {
const { url } = await createCheckoutSession(priceId);
if (url) window.location.href = url;
});
};
return (
<button
onClick={handleCheckout}
disabled={isPending}
className={`mt-6 w-full rounded-lg px-4 py-2.5 text-sm font-semibold ${
popular
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-900 text-white hover:bg-gray-800"
} disabled:opacity-50`}
>
{isPending ? "Chargement..." : isLoggedIn ? "S'abonner" : "Commencer"}
</button>
);
}Etape 7 : Creer les Server Actions pour le paiement
Creez src/app/pricing/actions.ts :
"use server";
import { auth } from "@/auth";
import { stripe } from "@/lib/stripe";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
export async function createCheckoutSession(priceId: string) {
const session = await auth();
if (!session?.user?.id) {
redirect("/login");
}
// Obtenir ou creer un client Stripe
const [user] = await db
.select()
.from(users)
.where(eq(users.id, session.user.id));
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email!,
name: user.name ?? undefined,
metadata: { userId: user.id },
});
await db
.update(users)
.set({ stripeCustomerId: customer.id })
.where(eq(users.id, user.id));
customerId = customer.id;
}
// Creer la session de paiement
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
subscription_data: {
metadata: { userId: session.user.id },
},
});
return { url: checkoutSession.url };
}
export async function createCustomerPortalSession() {
const session = await auth();
if (!session?.user?.id) {
redirect("/login");
}
const [user] = await db
.select()
.from(users)
.where(eq(users.id, session.user.id));
if (!user.stripeCustomerId) {
redirect("/pricing");
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
});
return { url: portalSession.url };
}Etape 8 : Gerer les webhooks Stripe
C'est la partie la plus critique. Les webhooks Stripe maintiennent votre base de donnees synchronisee avec les changements d'abonnement.
Creez src/app/api/webhooks/stripe/route.ts :
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
import type Stripe from "stripe";
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error("Echec de la verification de signature webhook:", err);
return new Response("Webhook Error", { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);
const userId = subscription.metadata.userId;
await db
.update(users)
.set({
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeSubscriptionStatus: subscription.status,
})
.where(eq(users.id, userId));
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await db
.update(users)
.set({
stripePriceId: subscription.items.data[0].price.id,
stripeSubscriptionStatus: subscription.status,
})
.where(eq(users.stripeSubscriptionId, subscription.id));
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await db
.update(users)
.set({
stripePriceId: null,
stripeSubscriptionId: null,
stripeSubscriptionStatus: "canceled",
})
.where(eq(users.stripeSubscriptionId, subscription.id));
break;
}
}
return new Response("OK", { status: 200 });
}Pour tester les webhooks localement, utilisez le CLI Stripe :
stripe listen --forward-to localhost:3000/api/webhooks/stripeGardez le CLI Stripe en marche dans un terminal separe pendant le developpement. Il transmettra les evenements webhook a votre serveur local et vous donnera un secret de signature webhook a utiliser dans .env.local.
Etape 9 : Construire le tableau de bord
Creez le tableau de bord protege dans src/app/dashboard/page.tsx :
import { auth, signOut } from "@/auth";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
import { PLANS } from "@/lib/stripe";
import { ManageSubscriptionButton } from "./manage-subscription-button";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const [user] = await db
.select()
.from(users)
.where(eq(users.id, session.user.id));
const currentPlan = PLANS.find((p) => p.priceId === user.stripePriceId);
const isActive = user.stripeSubscriptionStatus === "active";
return (
<div className="mx-auto max-w-4xl px-4 py-16">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Tableau de bord</h1>
<p className="text-gray-500">Bon retour, {session.user.name}</p>
</div>
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/" });
}}
>
<button
type="submit"
className="rounded-lg border px-4 py-2 text-sm hover:bg-gray-50"
>
Se deconnecter
</button>
</form>
</div>
<div className="mt-8 rounded-xl border p-6">
<h2 className="text-lg font-semibold">Abonnement</h2>
{isActive && currentPlan ? (
<div className="mt-4 space-y-4">
<div className="flex items-center gap-3">
<span className="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">
Actif
</span>
<span className="font-medium">Plan {currentPlan.name}</span>
<span className="text-gray-500">
{currentPlan.price}/mois
</span>
</div>
<ManageSubscriptionButton />
</div>
) : (
<div className="mt-4 space-y-4">
<p className="text-gray-500">
Vous etes actuellement sur le niveau gratuit.
</p>
<a
href="/pricing"
className="inline-block rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700"
>
Passer au niveau superieur
</a>
</div>
)}
</div>
<div className="mt-8 rounded-xl border p-6">
<h2 className="text-lg font-semibold">Compte</h2>
<div className="mt-4 space-y-2 text-sm">
<p>
<span className="text-gray-500">Email :</span> {user.email}
</p>
<p>
<span className="text-gray-500">Membre depuis :</span>{" "}
{user.createdAt?.toLocaleDateString("fr-FR")}
</p>
</div>
</div>
</div>
);
}Creez src/app/dashboard/manage-subscription-button.tsx :
"use client";
import { useTransition } from "react";
import { createCustomerPortalSession } from "../pricing/actions";
export function ManageSubscriptionButton() {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => {
startTransition(async () => {
const { url } = await createCustomerPortalSession();
if (url) window.location.href = url;
});
}}
disabled={isPending}
className="rounded-lg border px-4 py-2 text-sm font-medium hover:bg-gray-50 disabled:opacity-50"
>
{isPending ? "Chargement..." : "Gerer l'abonnement"}
</button>
);
}Etape 10 : Ajouter des verifications d'abonnement a votre API
Creez un utilitaire reutilisable dans src/lib/subscription.ts pour restreindre les fonctionnalites selon le plan :
import { auth } from "@/auth";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
export async function getUserSubscription() {
const session = await auth();
if (!session?.user?.id) return null;
const [user] = await db
.select({
stripePriceId: users.stripePriceId,
stripeSubscriptionStatus: users.stripeSubscriptionStatus,
})
.from(users)
.where(eq(users.id, session.user.id));
return {
isActive: user?.stripeSubscriptionStatus === "active",
priceId: user?.stripePriceId,
};
}
export async function requireSubscription() {
const sub = await getUserSubscription();
if (!sub?.isActive) {
throw new Error("Un abonnement actif est requis");
}
return sub;
}Utilisez-le dans n'importe quel Server Action ou route API :
"use server";
import { requireSubscription } from "@/lib/subscription";
export async function premiumFeatureAction() {
await requireSubscription(); // Lance une erreur si non abonne
// Votre logique premium ici
}Tester votre implementation
1. Tester le flux d'authentification
- Demarrez le serveur de developpement :
npm run dev - Visitez
http://localhost:3000/login - Connectez-vous avec GitHub ou Google
- Verifiez que vous etes redirige vers
/dashboard
2. Tester le flux d'abonnement
- Visitez
/pricinget cliquez sur un plan - Utilisez la carte de test Stripe :
4242 4242 4242 4242(date d'expiration future, n'importe quel CVC) - Completez le paiement et verifiez que le tableau de bord affiche votre plan actif
3. Tester les webhooks localement
# Terminal 1 : Demarrer Next.js
npm run dev
# Terminal 2 : Transmettre les evenements Stripe
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Terminal 3 : Declencher un evenement de test
stripe trigger checkout.session.completedDepannage
Erreur "No matching state found" pendant OAuth
Cela signifie generalement que votre AUTH_SECRET est manquant ou a change. Regenerez-le avec npx auth secret.
Les webhooks n'atteignent pas votre serveur local
Assurez-vous que le CLI Stripe est en marche avec stripe listen. Verifiez que le STRIPE_WEBHOOK_SECRET dans .env.local correspond au secret affiche par le CLI.
Le statut d'abonnement ne se met pas a jour
Verifiez les logs webhook dans le tableau de bord Stripe sous Developers > Webhooks. Assurez-vous que l'URL du endpoint webhook est correcte et que le secret de signature correspond.
Mise en production
Lors du deploiement en production, n'oubliez pas de :
- Configurer le endpoint webhook Stripe dans le tableau de bord Stripe pointant vers
https://votre-domaine.com/api/webhooks/stripe - Passer aux cles Stripe live — remplacez
sk_test_etpk_test_par vos cles de production - Configurer les URLs de redirection OAuth dans GitHub/Google pour votre domaine de production
- Definir toutes les variables d'environnement dans votre plateforme d'hebergement (Vercel, Railway, etc.)
- Activer le portail client Stripe dans votre tableau de bord Stripe sous Settings > Billing > Customer Portal
Prochaines etapes
Maintenant que vous avez un systeme de facturation SaaS fonctionnel, voici quelques idees pour l'etendre :
- Ajouter des notifications par email avec Resend ou SendGrid pour les evenements d'abonnement
- Implementer la facturation a l'usage avec Stripe metered billing pour les produits bases sur API
- Ajouter la gestion d'equipes avec le support des organisations et le controle d'acces par roles
- Construire un tableau de bord administrateur pour visualiser les metriques de revenus et les donnees clients
- Ajouter une periode d'essai gratuite en utilisant le parametre
trial_period_daysde Stripe
Conclusion
Vous avez construit un kit de demarrage SaaS complet avec Next.js 15, Auth.js v5 et Stripe. Cette base vous offre l'authentification OAuth, la facturation par abonnement avec plusieurs niveaux, la gestion d'etat pilotee par webhooks et un portail en libre-service pour les clients. A partir de la, vous pouvez vous concentrer sur la construction des fonctionnalites reelles de votre produit SaaS, sachant que l'infrastructure de facturation est solide.
Les patterns utilises dans ce tutoriel — Server Actions pour les mutations, middleware pour la protection des routes, et webhooks pour la synchronisation de l'etat — representent l'approche Next.js moderne qui s'adapte bien des projets personnels aux applications de production.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.