Mettre en place une authentification correcte est rarement la partie amusante d’un projet, et pourtant c’est souvent celle qui détermine si on livre dans les temps. better-auth est devenue en 2026 la bibliothèque de référence dans l’écosystème TypeScript moderne : framework-agnostic, sessions sécurisées par défaut, OAuth multi-provider, magic links, 2FA, multi-tenant. Couplée à TanStack Start et son plugin tanstackStartCookies, elle s’intègre en moins d’une heure. Ce tutoriel détaille l’installation, la config Drizzle, la route API catch-all, le client React, les helpers getSession, le pattern de protection de routes et l’ajout d’un provider OAuth (GitHub) en bonus.
Article de la série autour de TanStack Start en production 2026.
Prérequis
Avant de commencer, le projet doit déjà tourner sous TanStack Start. Si ce n’est pas le cas, le tutoriel Setup TanStack Start v1 couvre les étapes initiales.
- Node.js 22 LTS, npm 10+ ou pnpm 9+.
- Un projet TanStack Start v1 fonctionnel avec TypeScript.
- Une base de données accessible (PostgreSQL local, Neon, Supabase, ou D1 sur Cloudflare).
- Drizzle ORM installé et un schéma initial.
- Connaissances de base sur les sessions HTTP et les cookies.
Étape 1 — Installer better-auth et préparer Drizzle
Trois paquets sont nécessaires : better-auth pour la lib elle-même, drizzle-orm pour la couche d’accès base, et le driver correspondant à votre base (pg pour PostgreSQL, better-sqlite3 pour SQLite local ou rien pour D1 qui utilise l’API native).
npm install better-auth drizzle-orm
npm install pg
npm install -D drizzle-kit @types/pg
Une fois installé, on génère les variables d’environnement nécessaires. better-auth a besoin d’un secret pour signer les cookies de session et d’une URL de base. Créez un fichier .env à la racine.
# .env
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
Le secret doit être long et imprévisible — la commande openssl rand -base64 32 génère 32 octets aléatoires en base64. En production, ce secret vit dans les variables d’environnement de l’hébergeur (ou via wrangler secret put sur Cloudflare Workers comme expliqué dans le tutoriel Cloudflare Workers).
Étape 2 — Configurer better-auth avec Drizzle adapter
Le fichier de config central s’écrit une fois et alimente toute la suite. On y déclare l’adaptateur Drizzle qui pointe vers le schéma, le secret, l’URL de base et les plugins activés. Le plugin tanstackStartCookies est obligatoire et doit être le dernier de la liste : il intercepte les Set-Cookie et les renvoie via la réponse de la server function plutôt que via headers HTTP directs.
// src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { db } from '~/db/client'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
emailAndPassword: { enabled: true },
plugins: [tanstackStartCookies()], // doit rester en dernier
})
Le paramètre provider indique le SQL dialect cible : pg pour PostgreSQL, mysql, ou sqlite. Pour D1, utilisez sqlite avec un client Drizzle initialisé via drizzle-orm/d1. L’option emailAndPassword.enabled active la combinaison classique ; si vous voulez du passwordless uniquement, laissez à false et ajoutez le plugin magicLink à la place.
Étape 3 — Générer les tables d’authentification
better-auth crée automatiquement quatre tables : user, session, account, verification. Le schéma se génère via la CLI better-auth, qui produit un fichier Drizzle prêt à être importé. C’est l’approche recommandée parce qu’elle reste synchrone avec les évolutions de la bibliothèque.
npx @better-auth/cli generate --output ./src/db/auth-schema.ts
La commande lit votre auth.ts, voit qu’on utilise Drizzle, et écrit un fichier de schéma TypeScript avec les quatre tables et leurs relations. Vous l’importez ensuite dans votre db/schema.ts principal pour que drizzle-kit les inclue dans les migrations.
// src/db/schema.ts
export * from './auth-schema'
// ... vos autres tables métier
Lancez ensuite la migration habituelle de Drizzle pour créer les tables physiques. Le signal de succès : votre base contient bien les quatre tables, vérifiable avec psql ou un client GUI.
npx drizzle-kit generate
npx drizzle-kit migrate
Étape 4 — Exposer la route API catch-all /api/auth/$
better-auth fournit un handler unique qui répond à toutes les routes /api/auth/* : signin, signup, callback OAuth, etc. On expose ce handler via une route catch-all TanStack Router, qui capture toutes les requêtes derrière le préfixe et les délègue à auth.handler().
// src/routes/api/auth/$.ts
import { auth } from '~/lib/auth'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => auth.handler(request),
POST: async ({ request }: { request: Request }) => auth.handler(request),
},
},
})
Le splat $ attrape l’intégralité du chemin résiduel (/api/auth/sign-in/email, /api/auth/callback/github, etc.). better-auth dispatche en interne selon le chemin. Vous n’avez rien d’autre à écrire côté serveur — c’est le minimum d’intrusivité réclamé pour une auth full-stack.
Étape 5 — Créer le client React pour signIn/signUp/signOut
Côté navigateur, better-auth expose un client typé qu’on instancie une fois et qu’on importe partout. Il sait construire les bonnes URLs, gérer les redirections, parser les réponses. On l’isole dans un fichier dédié pour pouvoir le réutiliser depuis n’importe quel composant.
// src/lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_AUTH_URL,
})
export const { signIn, signUp, signOut, useSession } = authClient
Le hook useSession() est particulièrement pratique côté composant : il retourne { data, isPending, error } et se met à jour automatiquement après un signIn ou signOut. La session est aussi utilisable côté SSR via une server function dédiée, comme on va le voir.
Étape 6 — Construire un formulaire d’inscription
On combine TanStack Form avec le client auth pour un formulaire d’inscription propre. Le code suivant peut servir de base — adaptez-le à votre design système. La validation est faite par Zod côté client (UX) et par better-auth côté serveur (sécurité, contraintes uniqueness).
// src/routes/(auth)/signup.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'
import { signUp } from '~/lib/auth-client'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
})
export const Route = createFileRoute('/(auth)/signup')({
component: SignupPage,
})
function SignupPage() {
const router = useRouter()
const form = useForm({
defaultValues: { email: '', password: '', name: '' },
validators: { onChange: schema, onSubmit: schema },
onSubmit: async ({ value }) => {
const { error } = await signUp.email(value)
if (error) throw new Error(error.message)
router.navigate({ to: '/dashboard' })
},
})
return (
<form onSubmit={(e) => { e.preventDefault(); void form.handleSubmit() }}>
{/* form.Field pour email, password, name */}
</form>
)
}
L’appel signUp.email(value) envoie les données à /api/auth/sign-up/email ; better-auth crée l’utilisateur, hashe le mot de passe avec scrypt par défaut, ouvre une session, et renvoie le cookie. Le navigateur le stocke automatiquement, et au refresh suivant l’utilisateur reste connecté. Pour le pattern complet de formulaire avec Zod, voyez le tutoriel TanStack Form + Zod.
Étape 7 — Récupérer la session côté serveur
La session est utile aussi bien dans les beforeLoad que dans les server functions métier. Pour la lire côté serveur, on écrit un helper qui appelle l’API interne de better-auth avec les headers de la requête courante. C’est lui qu’on appellera partout où on a besoin de connaître l’utilisateur.
// src/lib/auth-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { auth } from './auth'
export const getSession = createServerFn({ method: 'GET' }).handler(async () => {
const headers = getRequestHeaders()
return await auth.api.getSession({ headers })
})
getRequestHeaders() récupère les headers de la requête HTTP en cours, dont le cookie de session. better-auth les inspecte, vérifie la signature, charge l’utilisateur depuis la table session et retourne { user, session } ou null. Cette fonction tourne uniquement côté serveur, jamais bundlée côté client — vos secrets restent là où ils doivent être.
Étape 8 — Protéger une zone d’application
Pour empêcher un utilisateur non connecté d’accéder au dashboard, on utilise le hook beforeLoad de TanStack Router. Il s’exécute avant le rendu de la route et peut lever redirect() pour envoyer l’utilisateur ailleurs. Le pattern idiomatique consiste à créer un layout pathless _protected.tsx qui fait la vérification une seule fois pour toutes ses routes filles.
// src/routes/_protected.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { getSession } from '~/lib/auth-functions'
export const Route = createFileRoute('/_protected')({
beforeLoad: async ({ location }) => {
const session = await getSession()
if (!session) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
return { user: session.user }
},
component: () => <Outlet />,
})
Toute route placée sous src/routes/_protected/ hérite de cette protection. Au premier accès non authentifié, l’utilisateur est redirigé vers /login?redirect=/dashboard. Une fois connecté, il revient au chemin original. Le return { user } rend l’utilisateur disponible dans le contexte du router : tout composant fils peut l’extraire via Route.useRouteContext().
Étape 9 — Ajouter un provider OAuth (GitHub)
Les providers OAuth se déclarent dans la config better-auth. Pour GitHub, créez une OAuth App dans les settings de votre compte GitHub avec l’URL callback http://localhost:3000/api/auth/callback/github. Récupérez le Client ID et le Client Secret dans .env.
// src/lib/auth.ts (extrait avec GitHub)
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { db } from '~/db/client'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [tanstackStartCookies()],
})
Côté composant, le bouton se résume à un onClick qui déclenche le flow OAuth. better-auth gère la redirection vers GitHub, le retour, l’échange du code, la création du user dans la table account liée au user, et l’ouverture de la session.
import { signIn } from '~/lib/auth-client'
<button
onClick={() => signIn.social({ provider: 'github', callbackURL: '/dashboard' })}
>
Continuer avec GitHub
</button>
Au clic, l’utilisateur part sur github.com/login/oauth/authorize, autorise l’app, revient sur /api/auth/callback/github, et atterrit sur /dashboard connecté. La même mécanique vaut pour les autres providers : Google, Apple, Discord, X, Microsoft, Facebook, etc. — better-auth en supporte une vingtaine en 2026.
Étape 10 — Déconnexion et nettoyage
La déconnexion est trivialement simple côté client. La méthode signOut() du client invalide la session côté serveur, supprime le cookie côté navigateur et remet useSession à null. On l’attache à un bouton et on redirige vers la home après.
import { signOut } from '~/lib/auth-client'
import { useRouter } from '@tanstack/react-router'
function LogoutButton() {
const router = useRouter()
return (
<button
onClick={async () => {
await signOut()
router.navigate({ to: '/' })
}}
>
Se déconnecter
</button>
)
}
Côté serveur, la session est immédiatement invalidée — un autre onglet qui appelle getSession() juste après reçoit null. Pour la sécurité, better-auth gère aussi le rate limiting des tentatives de connexion et l’expiration glissante des sessions sans configuration supplémentaire.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Cookie de session perdu | Plugin tanstackStartCookies manquant |
L’ajouter en dernier des plugins dans auth.ts |
Tables not found |
Migrations Drizzle non appliquées | Lancer drizzle-kit generate puis migrate |
| OAuth callback 404 | URL callback mal configurée chez le provider | Vérifier BETTER_AUTH_URL + URL callback côté GitHub/Google |
| Session ne survit pas au refresh | Cookie domain ou secure mal configuré | En prod, utiliser HTTPS et ajuster cookieOptions |
| Erreur CORS sur OAuth | baseURL ne correspond pas à l’origine réelle |
Aligner BETTER_AUTH_URL avec l’URL de l’app |
| signIn.social ne déclenche pas la redirection | Provider non déclaré dans socialProviders |
Ajouter le provider dans auth.ts |
FAQ
Faut-il une server function pour chaque appel auth ? Non, le client better-auth/react les expose déjà tous (signIn, signUp, signOut, updateUser, etc.). Réservez les server functions aux opérations métier qui nécessitent l’utilisateur courant.
Comment activer la 2FA ? Ajoutez le plugin twoFactor() dans plugins: [...]. Le client expose alors authClient.twoFactor.enable() qui retourne un QR code pour l’authenticator.
Peut-on personnaliser la table user (champ role, orgId) ? Oui, via l’option user: { additionalFields: { role: { type: 'string' } } } dans la config. better-auth ajoute ces colonnes lors de la prochaine génération de schéma.
Comment gérer les organisations multi-tenant ? Utilisez le plugin organization() qui crée les tables organization, member, invitation et expose des helpers pour gérer les rôles.
better-auth fonctionne-t-il sur Cloudflare Workers ? Oui, à condition d’utiliser un client Drizzle compatible (D1 ou Hyperdrive vers PostgreSQL). Voir le tutoriel sur le déploiement Cloudflare.
Pour aller plus loin
- L’article principal : TanStack Start en production 2026.
- Pour les server functions et le pattern RPC : server functions TanStack Start.
- Pour les formulaires de signin/signup : TanStack Form + Zod.
- Pour le routage protégé : file-based routing avec TanStack Router.
Ressources
- Documentation officielle better-auth : better-auth.com/docs/introduction
- Intégration TanStack Start : better-auth.com/docs/integrations/tanstack
- Drizzle ORM : orm.drizzle.team
- Template de référence Daveyplate : github.com/daveyplate/better-auth-tanstack-starter