Créer un SaaS avec Next.js et Stripe en 2026 : Du MVP au Premier Client
Meta description : Guide complet pour créer votre SaaS avec Next.js et Stripe en 2026. Architecture, authentification, paiements, déploiement — tout ce qu'il faut pour lancer votre produit. Mots-clés : créer saas nextjs, saas stripe 2026, boilerplate saas, lancer startup saas, next.js stripe tutorialPourquoi Next.js + Stripe est le stack SaaS de référence en 2026
Créer un SaaS (Software as a Service) n'a jamais été aussi accessible. En 2026, le combo Next.js + Stripe est devenu le standard pour les entrepreneurs tech qui veulent aller vite sans sacrifier la qualité. Next.js fournit le frontend et le backend dans un seul framework, tandis que Stripe gère tout le cycle de paiement — abonnements, factures, remboursements, conformité fiscale.
Le marché SaaS mondial atteint 300 milliards de dollars en 2026, avec une croissance annuelle de 18%. Les micro-SaaS (produits ciblant une niche spécifique) représentent une opportunité particulièrement attractive pour les développeurs solo ou les petites équipes : coûts de lancement proches de zéro, revenus récurrents, et scalabilité quasi infinie.
Architecture d'un SaaS moderne avec Next.js
Stack technique recommandé
| Couche | Technologie | Pourquoi |
|--------|------------|----------|
| Frontend | Next.js 15 + React 19 | SSR, App Router, Server Components |
| Styling | Tailwind CSS v4 | Rapide, responsive, dark mode |
| Auth | NextAuth.js / Clerk | OAuth, magic links, sessions |
| Base de données | PostgreSQL + Prisma | Type-safe, migrations, relations |
| Paiements | Stripe | Abonnements, checkout, webhooks |
| Emails | Resend / SendGrid | Transactional emails, templates |
| Hébergement | Vercel / VPS | Edge, serverless ou dédié |
| Monitoring | Sentry | Erreurs, performance, alertes |
Structure du projet
my-saas/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ ├── settings/page.tsx
│ │ ├── billing/page.tsx
│ │ └── layout.tsx
│ ├── (marketing)/
│ │ ├── page.tsx # Landing page
│ │ ├── pricing/page.tsx
│ │ ├── blog/page.tsx
│ │ └── layout.tsx
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── stripe/
│ │ │ ├── checkout/route.ts
│ │ │ ├── webhook/route.ts
│ │ │ └── portal/route.ts
│ │ └── trpc/[trpc]/route.ts
│ ├── layout.tsx
│ └── globals.css
├── components/
├── lib/
│ ├── db.ts
│ ├── stripe.ts
│ └── auth.ts
├── prisma/
│ └── schema.prisma
└── package.json
Étape 1 : Configuration du projet
Initialiser Next.js
npx create-next-app@latest my-saas --typescript --tailwind --eslint --app --src-dir
cd my-saas
Installer les dépendances essentielles
# Base de données
npm install @prisma/client
npx prisma init
Authentification
npm install next-auth @auth/prisma-adapter
Paiements
npm install stripe @stripe/stripe-js
UI
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
npm install lucide-react class-variance-authority clsx tailwind-merge
Configuration Prisma (schema.prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
// Stripe
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
// Relations
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
Étape 2 : Authentification avec NextAuth.js
Configuration NextAuth
// lib/auth.ts
import { PrismaAdapter } from "@auth/prisma-adapter"
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GithubProvider from "next-auth/providers/github"
import EmailProvider from "next-auth/providers/email"
import { db } from "./db"
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
pages: {
signIn: "/login",
},
callbacks: {
async session({ session, user }) {
if (session.user) {
session.user.id = user.id
}
return session
},
},
}
Page de connexion
// app/(auth)/login/page.tsx
"use client"
import { signIn } from "next-auth/react"
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-6 rounded-xl border p-8">
<h1 className="text-2xl font-bold text-center">Connexion</h1>
<button
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
className="w-full rounded-lg bg-white border px-4 py-3 font-medium hover:bg-gray-50"
>
Continuer avec Google
</button>
<button
onClick={() => signIn("github", { callbackUrl: "/dashboard" })}
className="w-full rounded-lg bg-gray-900 px-4 py-3 font-medium text-white hover:bg-gray-800"
>
Continuer avec GitHub
</button>
</div>
</div>
)
}
Étape 3 : Intégration Stripe pour les abonnements
Configuration Stripe
// lib/stripe.ts
import Stripe from "stripe"
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
typescript: true,
})
export const PLANS = {
free: {
name: "Free",
price: 0,
features: ["5 projets", "1 Go stockage", "Support communauté"],
},
pro: {
name: "Pro",
stripePriceId: process.env.STRIPE_PRO_PRICE_ID!,
price: 19,
features: ["Projets illimités", "50 Go stockage", "Support prioritaire", "API access"],
},
enterprise: {
name: "Enterprise",
stripePriceId: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
price: 49,
features: ["Tout Pro +", "Stockage illimité", "Support dédié", "SSO", "SLA 99.9%"],
},
}
Créer une session Checkout
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from "next/server"
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 })
}
const { priceId } = await req.json()
const user = await db.user.findUnique({ where: { id: session.user.id } })
// Créer ou récupérer le customer Stripe
let customerId = user?.stripeCustomerId
if (!customerId) {
const customer = await stripe.customers.create({
email: session.user.email!,
metadata: { userId: session.user.id },
})
customerId = customer.id
await db.user.update({
where: { id: session.user.id },
data: { stripeCustomerId: customerId },
})
}
// Créer la session Checkout
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,
metadata: { userId: session.user.id },
})
return NextResponse.json({ url: checkoutSession.url })
}
Gérer les webhooks Stripe
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from "next/server"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
export async function POST(req: NextRequest) {
const body = await req.text()
const signature = req.headers.get("stripe-signature")!
let event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
)
await db.user.update({
where: { stripeCustomerId: session.customer as string },
data: {
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
})
break
}
case "invoice.payment_succeeded": {
const invoice = event.data.object
if (invoice.subscription) {
const subscription = await stripe.subscriptions.retrieve(
invoice.subscription as string
)
await db.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
})
}
break
}
case "customer.subscription.deleted": {
const subscription = event.data.object
await db.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
stripeSubscriptionId: null,
stripePriceId: null,
stripeCurrentPeriodEnd: null,
},
})
break
}
}
return NextResponse.json({ received: true })
}
Étape 4 : Page de pricing
// app/(marketing)/pricing/page.tsx}"use client"
import { useSession } from "next-auth/react"
import { PLANS } from "@/lib/stripe"
export default function PricingPage() {
const { data: session } = useSession()
const handleSubscribe = async (priceId: string) => {
const res = await fetch("/api/stripe/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
})
const { url } = await res.json()
window.location.href = url
}
return (
<div className="py-24">
<h1 className="text-4xl font-bold text-center mb-4">
Choisissez votre plan
</h1>
<p className="text-center text-gray-600 mb-12">
Commencez gratuitement, upgradez quand vous êtes prêt
</p>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto px-4">
{Object.entries(PLANS).map(([key, plan]) => (
<div
key={key}
className={
rounded-2xl border p-8 ${key === "pro" ? "border-blue-500 ring-2 ring-blue-500" : ""
}
>
{key === "pro" && (
<span className="text-sm font-medium text-blue-600 mb-4 block">
Plus populaire
</span>
)}
<h3 className="text-xl font-bold">{plan.name}</h3>
<div className="mt-4 mb-6">
<span className="text-4xl font-bold">{plan.price}€</span>
{plan.price > 0 && <span className="text-gray-500">/mois</span>}
</div>
<ul className="space-y-3 mb-8">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<span className="text-green-500">✓</span>
{feature}
</li>
))}
</ul>
<button
onClick={() =>
"stripePriceId" in plan
? handleSubscribe(plan.stripePriceId)
: null
}
className={
w-full rounded-lg px-4 py-3 font-medium ${key === "pro"
? "bg-blue-600 text-white hover:bg-blue-700"
: "border hover:bg-gray-50"
}}
>
{plan.price === 0 ? "Commencer gratuitement" : "S'abonner"}
</button>
</div>
))}
</div>
</div>
)
}
Étape 5 : Dashboard utilisateur
// app/(dashboard)/dashboard/page.tsx
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { redirect } from "next/navigation"
export default async function DashboardPage() {
const session = await getServerSession(authOptions)
if (!session?.user?.id) redirect("/login")
const user = await db.user.findUnique({
where: { id: session.user.id },
})
const isPro = user?.stripePriceId != null
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-6">
Bienvenue, {session.user.name}
</h1>
<div className="grid md:grid-cols-3 gap-6">
<div className="rounded-xl border p-6">
<h3 className="text-sm font-medium text-gray-500">Plan actuel</h3>
<p className="text-2xl font-bold mt-2">
{isPro ? "Pro" : "Free"}
</p>
</div>
<div className="rounded-xl border p-6">
<h3 className="text-sm font-medium text-gray-500">Projets</h3>
<p className="text-2xl font-bold mt-2">3 / {isPro ? "∞" : "5"}</p>
</div>
<div className="rounded-xl border p-6">
<h3 className="text-sm font-medium text-gray-500">Stockage</h3>
<p className="text-2xl font-bold mt-2">
245 Mo / {isPro ? "50 Go" : "1 Go"}
</p>
</div>
</div>
</div>
)
}
Étape 6 : Déploiement
Option A — Vercel (le plus simple)
# Installer Vercel CLI
npm i -g vercel
Déployer
vercel
Variables d'environnement sur Vercel Dashboard
DATABASE_URL, STRIPE_SECRET_KEY, NEXTAUTH_SECRET, etc.
Option B — VPS avec Docker (plus de contrôle)
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Gagner du temps : utiliser un boilerplate SaaS
Construire tout depuis zéro prend du temps — entre l'authentification, les paiements, les webhooks, le dashboard, les emails transactionnels... comptez facilement 2 à 4 semaines avant d'écrire la première ligne de votre feature principale.
C'est pourquoi de plus en plus de développeurs utilisent des boilerplates SaaS qui intègrent tout ce plumbing de base. Notre SaaS Boilerplate React + Symfony vous donne :
- Authentification complète (OAuth, magic links)
- Intégration Stripe (abonnements, checkout, webhooks)
- Dashboard admin prêt à l'emploi
- Emails transactionnels
- Déploiement Docker one-click
Découvrez tous nos templates premium pour développeurs.
Métriques clés à suivre pour votre SaaS
| Métrique | Formule | Objectif |
|----------|---------|----------|
| MRR (Monthly Recurring Revenue) | Somme des abonnements actifs | Croissance mensuelle |
| Churn Rate | Clients perdus / Total clients × 100 | < 5% /mois |
| LTV (Lifetime Value) | ARPU / Churn Rate | > 3× CAC |
| CAC (Coût d'Acquisition Client) | Budget marketing / Nouveaux clients | < LTV/3 |
| Conversion (Trial → Paid) | Clients payants / Inscrits × 100 | > 5% |
Erreurs courantes à éviter
Conclusion
Créer un SaaS avec Next.js et Stripe en 2026 est à la portée de tout développeur motivé. Le stack est mature, les outils sont disponibles, et le marché est en croissance. La clé du succès ? Lancer vite, écouter les utilisateurs, et itérer.
Votre checklist de lancement :Le meilleur moment pour lancer votre SaaS était hier. Le deuxième meilleur moment, c'est maintenant.
FAQ
Combien coûte le lancement d'un SaaS ?
Avec des outils gratuits (Vercel free tier, Supabase free tier, Stripe sans abonnement), vous pouvez lancer un SaaS pour moins de 20€/mois (domaine + email). Les coûts augmentent avec le trafic.
Next.js ou Remix pour un SaaS ?
Next.js a un écosystème plus large, plus de boilerplates, et un meilleur support Vercel. Remix est excellent mais plus niche. Pour un premier SaaS, Next.js est le choix le plus sûr.
Stripe ou Lemon Squeezy ?
Stripe est plus puissant et flexible (webhooks, API complète, Connect). Lemon Squeezy simplifie la TVA/taxes mais est plus limité. Pour un SaaS sérieux, Stripe est recommandé.
Faut-il une société pour vendre un SaaS ?
En France, vous pouvez commencer en micro-entreprise (auto-entrepreneur). Au-delà de 77 700€/an de CA en services, il faudra envisager une SASU ou EURL.
Combien de temps pour atteindre 1 000€ MRR ?
Avec un bon positionnement et du marketing actif, 3 à 6 mois est réaliste. La clé est d'avoir un pricing correct (>19€/mois) et un canal d'acquisition fiable (SEO, communauté, Product Hunt).