Better Auth est la bibliothèque d'authentification que Vercel a discrètement acquise en juillet 2026, et pour de bonnes raisons. Elle est indépendante du framework, TypeScript-first, et propose plus de fonctionnalités clés en main que n'importe quel concurrent — email/mot de passe, OAuth, double authentification, gestion des sessions et un écosystème de plugins croissant. Ce tutoriel vous guide pas à pas dans la construction d'un système d'authentification prêt pour la production dans un projet Next.js 15 App Router.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou une version plus récente
- Une base de données PostgreSQL (locale via Docker ou hébergée comme Neon ou Supabase)
- Une connaissance de base de Next.js App Router et TypeScript
- Une application GitHub OAuth pour la connexion sociale (nous la créerons pendant le tutoriel)
pnpminstallé globalement (npm install -g pnpm)
Ce que vous allez construire
À la fin de ce tutoriel, vous disposerez d'une application Next.js 15 avec :
- Inscription et connexion par email et mot de passe
- Connexion sociale via GitHub OAuth
- Gestion des sessions basée sur JWT avec actualisation automatique
- Accès côté serveur à la session dans les React Server Components
- Protection des routes via Middleware
- Un flux de déconnexion avec redirection
Étape 1 : Créer le projet Next.js 15
Démarrez un nouveau projet Next.js 15 avec App Router et TypeScript :
pnpm create next-app@latest my-auth-app --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"
cd my-auth-appUne fois l'échafaudage terminé, ouvrez le projet dans votre éditeur.
Étape 2 : Installer Better Auth et les dépendances
Installez Better Auth avec Drizzle ORM et le client PostgreSQL :
pnpm add better-auth drizzle-orm @auth/drizzle-adapter
pnpm add pg
pnpm add -D drizzle-kit @types/pgCréez votre .env.local avec les variables d'environnement requises. Better Auth a besoin d'un secret pour signer les sessions :
# Générer un secret fort
openssl rand -base64 32Ajoutez ensuite les lignes suivantes dans .env.local :
# Base de données
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp
# Better Auth
BETTER_AUTH_SECRET=votre-secret-généré
BETTER_AUTH_URL=http://localhost:3000
# GitHub OAuth (Étape 11)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=Étape 3 : Configurer la base de données avec Drizzle ORM
Créez le fichier de configuration Drizzle à la racine du projet :
// drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./lib/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;Créez ensuite la connexion à la base de données :
// lib/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
});
export const db = drizzle(pool);Better Auth peut générer son propre schéma automatiquement. Exécutez le CLI pour produire les fichiers de migration :
pnpm dlx better-auth generate --output ./lib/db/schema.tsCela crée les tables user, session, account et verification. Appliquez ensuite la migration :
pnpm drizzle-kit pushÉtape 4 : Configurer le serveur Better Auth
Créez le fichier de configuration central de l'authentification. C'est ici que vous connectez la base de données, activez les fournisseurs et configurez les options :
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/lib/db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: false, // à true en production
minPasswordLength: 8,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 jours en secondes
updateAge: 60 * 60 * 24, // actualiser si plus vieux d'un jour
},
});La fonction betterAuth retourne une instance d'authentification entièrement configurée qui gère toute la cryptographie, la rotation des tokens et les interactions avec la base de données.
Étape 5 : Créer le gestionnaire de route API
Better Auth utilise une route API catch-all pour gérer tous les points de terminaison d'authentification. Créez ce fichier :
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);Ce fichier unique gère automatiquement tous les endpoints suivants :
POST /api/auth/sign-up/emailPOST /api/auth/sign-in/emailPOST /api/auth/sign-outGET /api/auth/sessionGET /api/auth/callback/github- Et bien d'autres
Étape 6 : Initialiser le client d'authentification
L'instance cliente encapsule toutes les actions d'authentification pour une utilisation dans les composants React. Créez-la une seule fois et importez-la là où vous en avez besoin :
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
});
// Exports nommés pour la commodité
export const {
signIn,
signUp,
signOut,
useSession,
getSession,
} = authClient;Ajoutez également NEXT_PUBLIC_APP_URL=http://localhost:3000 dans .env.local.
Étape 7 : Construire la page d'inscription
Créez une page d'inscription qui collecte le nom, l'email et le mot de passe de l'utilisateur :
// app/(auth)/sign-up/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
const data = new FormData(e.currentTarget);
const name = data.get("name") as string;
const email = data.get("email") as string;
const password = data.get("password") as string;
const { error: signUpError } = await signUp.email({
name,
email,
password,
callbackURL: "/dashboard",
});
if (signUpError) {
setError(signUpError.message ?? "Inscription échouée. Veuillez réessayer.");
setLoading(false);
return;
}
router.push("/dashboard");
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-md bg-white rounded-2xl shadow p-8">
<h1 className="text-2xl font-bold mb-6">Créer votre compte</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Nom</label>
<input
name="name"
type="text"
required
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
name="email"
type="email"
required
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Mot de passe</label>
<input
name="password"
type="password"
required
minLength={8}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-red-600 text-sm">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white rounded-lg py-2 font-medium hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Création en cours…" : "S'inscrire"}
</button>
</form>
<p className="mt-4 text-sm text-center text-gray-600">
Vous avez déjà un compte ?{" "}
<Link href="/sign-in" className="text-blue-600 hover:underline">
Se connecter
</Link>
</p>
</div>
</div>
);
}Étape 8 : Construire la page de connexion
La page de connexion suit le même schéma, avec en plus le bouton GitHub OAuth :
// app/(auth)/sign-in/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleEmailSignIn(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
const data = new FormData(e.currentTarget);
const { error: signInError } = await signIn.email({
email: data.get("email") as string,
password: data.get("password") as string,
callbackURL: "/dashboard",
});
if (signInError) {
setError(signInError.message ?? "Identifiants invalides.");
setLoading(false);
return;
}
router.push("/dashboard");
}
async function handleGitHubSignIn() {
await signIn.social({
provider: "github",
callbackURL: "/dashboard",
});
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-md bg-white rounded-2xl shadow p-8">
<h1 className="text-2xl font-bold mb-6">Se connecter à votre compte</h1>
<button
onClick={handleGitHubSignIn}
className="w-full flex items-center justify-center gap-2 border rounded-lg py-2 mb-4 hover:bg-gray-50"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
Continuer avec GitHub
</button>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">ou</span>
</div>
</div>
<form onSubmit={handleEmailSignIn} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
name="email"
type="email"
required
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Mot de passe</label>
<input
name="password"
type="password"
required
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-red-600 text-sm">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white rounded-lg py-2 font-medium hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Connexion en cours…" : "Se connecter"}
</button>
</form>
<p className="mt-4 text-sm text-center text-gray-600">
Pas encore de compte ?{" "}
<Link href="/sign-up" className="text-blue-600 hover:underline">
S'inscrire
</Link>
</p>
</div>
</div>
);
}Étape 9 : Protéger les routes avec Middleware
Utilisez le middleware Next.js pour rediriger les utilisateurs non authentifiés loin des pages protégées :
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
const { pathname } = request.nextUrl;
// Rediriger les utilisateurs authentifiés loin des pages d'auth
if (sessionCookie && ["/sign-in", "/sign-up"].includes(pathname)) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
// Protéger les routes du tableau de bord
if (!sessionCookie && pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/sign-in", "/sign-up"],
};L'utilitaire getSessionCookie est intentionnellement léger — il lit la valeur du cookie sans appel à la base de données, donc le middleware s'exécute en moins d'une milliseconde à la périphérie.
Étape 10 : Accéder à la session dans les Server Components
Dans n'importe quel React Server Component, vous pouvez récupérer la session complète depuis la base de données :
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold">
Bienvenue, {session.user.name}
</h1>
<p className="text-gray-500 mt-2">{session.user.email}</p>
</main>
);
}Étape 11 : Ajouter GitHub OAuth
Vous avez déjà configuré GITHUB_CLIENT_ID et GITHUB_CLIENT_SECRET à l'étape 4. Créez maintenant l'application GitHub OAuth :
- Allez dans GitHub Settings puis Developer settings puis OAuth Apps puis New OAuth App
- Définissez Homepage URL sur
http://localhost:3000 - Définissez Authorization callback URL sur
http://localhost:3000/api/auth/callback/github - Copiez le Client ID et générez un Client secret
- Collez les deux valeurs dans votre
.env.local
Le bouton de connexion GitHub ajouté à l'étape 8 appelle déjà signIn.social({ provider: "github" }). Better Auth gère tout le flux OAuth pour vous.
Étape 12 : Ajouter le bouton de déconnexion
Ajoutez un bouton de déconnexion à votre tableau de bord ou à votre barre de navigation :
// components/SignOutButton.tsx
"use client";
import { useRouter } from "next/navigation";
import { signOut } from "@/lib/auth-client";
export function SignOutButton() {
const router = useRouter();
async function handleSignOut() {
await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/sign-in");
},
},
});
}
return (
<button
onClick={handleSignOut}
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-red-600"
>
Se déconnecter
</button>
);
}Étape 13 : Accéder à la session dans les Client Components
Pour les composants qui doivent réagir à l'état d'authentification, utilisez le hook useSession :
// components/UserAvatar.tsx
"use client";
import { useSession } from "@/lib/auth-client";
export function UserAvatar() {
const { data: session, isPending } = useSession();
if (isPending) {
return <div className="w-8 h-8 rounded-full bg-gray-200 animate-pulse" />;
}
if (!session) {
return null;
}
return (
<div className="flex items-center gap-2">
{session.user.image ? (
<img
src={session.user.image}
alt={session.user.name}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
{session.user.name.charAt(0).toUpperCase()}
</div>
)}
<span className="text-sm font-medium">{session.user.name}</span>
</div>
);
}Le hook useSession maintient la session synchronisée avec le serveur — si la session expire ou si l'utilisateur se déconnecte dans un autre onglet, le hook se met à jour automatiquement.
Tester votre implémentation
Démarrez le serveur de développement et vérifiez chaque flux :
pnpm devInscription par email : Naviguez vers http://localhost:3000/sign-up, remplissez le formulaire et confirmez que vous êtes redirigé vers /dashboard.
Déconnexion : Cliquez sur le bouton de déconnexion et vérifiez que vous arrivez sur /sign-in.
Connexion : Retournez à http://localhost:3000/sign-in et connectez-vous avec les identifiants que vous venez de créer.
Protection des routes : Étant déconnecté, naviguez directement vers http://localhost:3000/dashboard — vous devriez être redirigé vers /sign-in.
GitHub OAuth : Cliquez sur Continuer avec GitHub, autorisez l'application et confirmez la redirection vers /dashboard en tant qu'utilisateur connecté.
Résolution des problèmes courants
BETTER_AUTH_SECRET manquant : Le serveur s'arrête au démarrage si le secret n'est pas défini. Assurez-vous que .env.local est chargé et que le nom de la variable correspond exactement.
Non-correspondance de l'URL de callback OAuth : GitHub renvoie une erreur redirect_uri_mismatch si l'URL de callback dans vos paramètres d'application GitHub OAuth ne correspond pas exactement. Pour le développement local, utilisez http://localhost:3000/api/auth/callback/github.
Session non actualisée : Par défaut, Better Auth n'actualise la session que si elle est plus ancienne que updateAge secondes. En développement, vous pouvez définir updateAge: 0 pour forcer une actualisation à chaque requête.
Prochaines étapes
- Activer la vérification par email : Définissez
requireEmailVerification: truedansemailAndPasswordet implémentezsendVerificationEmailavec un service d'email transactionnel comme Resend ou Mailgun. - Ajouter la double authentification : Better Auth dispose d'un plugin 2FA intégré. Installez
@better-auth/2faet ajoutez-le au tableaupluginsdans votre configuration. - Contrôle d'accès basé sur les rôles : Utilisez le plugin
@better-auth/rbacpour définir les rôles et les permissions directement dans votre configuration. - Déployer sur Vercel : Définissez toutes les variables d'environnement dans les paramètres de votre projet Vercel. Better Auth fonctionne parfaitement avec les fonctions serverless et Edge Runtime de Vercel.
Conclusion
Vous disposez maintenant d'un système d'authentification complet construit sur Better Auth et Next.js 15. La bibliothèque a pris en charge toutes les parties complexes — le hachage sécurisé des mots de passe, la rotation des tokens de session, la vérification de l'état OAuth et la protection CSRF — vous permettant de vous concentrer sur les fonctionnalités uniques de votre application. Alors que Vercel continue d'investir dans Better Auth suite à son acquisition, attendez-vous à une intégration plus étroite avec l'écosystème Vercel et une adoption encore plus rapide au sein de la communauté TypeScript.