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 tutorial

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

Résultat : vous passez de 4 semaines à 4 heures pour avoir un SaaS fonctionnel. Le reste du temps, vous le consacrez à ce qui compte vraiment — votre produit.

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

  • Construire trop avant de vendre : lancez un MVP minimal et itérez avec les retours clients
  • Ignorer le SEO : le trafic organique est gratuit et durable. Créez un blog dès le jour 1
  • Pricing trop bas : ne sous-évaluez pas votre produit. Commencez à 19€/mois minimum
  • Pas de webhooks Stripe : sans webhooks, vous ne synchronisez pas les statuts de paiement
  • Négliger l'onboarding : les 5 premières minutes de l'utilisateur déterminent la conversion
  • 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 :
  • Validez votre idée (parlez à 10 utilisateurs potentiels)
  • Choisissez votre stack (Next.js + Stripe + PostgreSQL)
  • Utilisez un boilerplate pour gagner du temps
  • Déployez un MVP en 2-4 semaines
  • Lancez sur Product Hunt + réseaux sociaux
  • Mesurez, itérez, scalez
  • 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).