Stack technique
Better Auth + Convex : auth passwordless en 30 minutes
Tutoriel complet pour brancher Better Auth sur Convex avec magic link et OAuth Google. Configuration, sécurité, cookies web vs bearer mobile. Code copiable.
Le mot de passe est mort. En 2026, magic link et OAuth sont la norme : pas de password à oublier, pas de reset à débugger, pas de hash bcrypt à gérer. Voici comment brancher Better Auth sur Convex en 30 minutes — la stack exacte que j'utilise dans ApexKit, validée en production.
Pourquoi Better Auth ?
Better Auth est la lib auth qui monte dans l'écosystème TypeScript. C'est l'alternative moderne à NextAuth/Auth.js et Clerk, avec trois différences majeures :
100 % code, zéro hosted. Pas de vendor lock-in, pas de coût à l'utilisateur.
Type-safe end-to-end. Les sessions, users, plugins — tout est typé.
Plugin architecture. Magic link, OAuth, 2FA, organizations, passkeys — tu actives ce dont tu as besoin.
Et le combo Better Auth + @convex-dev/better-auth s'intègre nativement à Convex : sessions stockées en Convex DB, queries authentifiées, hooks React tout prêts.
L'architecture cible
Voici ce qu'on construit. Trois couches :
Better Auth core — gère sessions, OAuth providers, magic links, validation des tokens.
Composant Convex — stocke les users + sessions dans la DB Convex, expose une API typée.
Client React
authClient— appelé depuis les composants pour signIn/signOut/useSession.
Pour le mobile (Expo), on remplace les cookies par des bearer tokens stockés en SecureStore. C'est la seule différence.
Étape 1 — Installer les dépendances
# Dans le package backend
pnpm add better-auth @convex-dev/better-auth
# Dans le package web
pnpm add better-auth @convex-dev/better-authÉtape 2 — Activer le composant Convex
// convex/convex.config.ts
import { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";
const app = defineApp();
app.use(betterAuth);
export default app;Le composant betterAuth crée automatiquement les tables users, sessions, accounts et verifications dans ta DB Convex.
Étape 3 — Configurer Better Auth
// convex/auth.ts
import { BetterAuth } from "@convex-dev/better-auth";
import { components } from "./_generated/api";
import { magicLink, oAuthProxy } from "better-auth/plugins";
export const betterAuth = new BetterAuth(components.betterAuth, {
options: {
baseURL: process.env.SITE_URL!,
secret: process.env.BETTER_AUTH_SECRET!,
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [
magicLink({
sendMagicLink: async ({ email, token, url }) => {
// → ton envoi via Resend / Postmark / etc.
await sendMagicLinkEmail({ email, url });
},
}),
oAuthProxy(),
],
},
});Étape 4 — Brancher le route handler
Côté TanStack Start (ou Next.js), tu exposes une seule route /api/auth/* qui forwarde vers Better Auth :
// src/routes/api/auth/$.tsx
import { createServerFileRoute } from "@tanstack/react-start/server";
import { betterAuth } from "@apexkit/backend/convex/auth";
export const ServerRoute = createServerFileRoute("/api/auth/$").methods({
GET: ({ request }) => betterAuth.handler(request),
POST: ({ request }) => betterAuth.handler(request),
});Cette route gère : OAuth callbacks, magic link verifications, signout, session refresh. Une seule route, tout le reste est config.
Étape 5 — Client React
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { convexClient } from "@convex-dev/better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SITE_URL,
plugins: [convexClient()],
});
export const { signIn, signOut, useSession } = authClient;Dans tes composants :
function LoginButton() {
const handleMagicLink = async () => {
await signIn.magicLink({
email: "user@example.com",
callbackURL: "/dashboard",
});
};
return <button onClick={handleMagicLink}>Email me a link</button>;
}
function UserBadge() {
const { data: session } = useSession();
if (!session) return null;
return <span>Connecté : {session.user.email}</span>;
}Étape 6 — Queries Convex authentifiées
Dans une query/mutation Convex, tu récupères l'utilisateur courant via :
// convex/dashboard.ts
import { query } from "./_generated/server";
import { betterAuth } from "./auth";
export const me = query({
args: {},
handler: async (ctx) => {
const user = await betterAuth.getAuthUser(ctx);
if (!user) throw new Error("Non authentifié");
return user;
},
});Important : getAuthUser est appelé dans la query — donc l'authz est ré-évaluée à chaque update réactif. Tu ne peux pas oublier de checker.
Cookies web vs Bearer mobile
Côté web (TanStack/Next.js), Better Auth utilise des cookies HTTP-only sécurisés. C'est automatique.
Côté mobile (Expo / React Native), les cookies ne marchent pas bien — Better Auth fournit un plugin expoClient qui utilise des bearer tokens stockés dans expo-secure-store (Keychain iOS / Keystore Android). Le token est envoyé en header Authorization: Bearer <token>.
// mobile/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_SITE_URL,
plugins: [
expoClient({
scheme: "apexkit",
storagePrefix: "apexkit",
storage: SecureStore,
}),
],
});Le scheme deep link (apexkit://) permet de gérer les callbacks OAuth depuis l'app mobile.
Pièges classiques à éviter
Le redirect URI Google sur Nitro
Si ton frontend tourne sur TanStack Start (Nitro proxy), l'URL de redirect OAuth Google doit être {SITE_URL}/api/auth/callback/google — pas l'URL Convex en .convex.site. Sinon Google rejette le callback.
Le secret Better Auth
Génère-le avec openssl rand -base64 32 et stocke-le dans tes env Convex via npx convex env set BETTER_AUTH_SECRET .... Ne jamais commit.
Magic link en local
En dev, tu n'as pas Resend configuré. Loggue simplement le lien dans la console :
sendMagicLink: async ({ url }) => {
if (process.env.NODE_ENV === "development") {
console.log("🔗 Magic link:", url);
return;
}
await sendViaResend({ /* ... */ });
}Pourquoi cette stack écrase tout le reste
En 30 minutes, tu as :
Auth passwordless (magic link) + Google OAuth
Sessions persistées dans ta DB Convex (pas de Redis externe)
Type safety end-to-end (impossible d'oublier un check d'authz)
Web + mobile via le même backend
Zéro coût par utilisateur (vs Clerk à 0.02$/MAU)
Pas de vendor lock-in (lib open-source MIT)
Le seul travail restant : design des écrans login/signup et email templates. Et encore — Better Auth fournit des composants par défaut acceptables.
Va plus vite : récupère la stack câblée
ApexKit livre cette stack préconfigurée : Better Auth + Convex + magic link + Google OAuth + emails Resend + sessions sécurisées + mobile bearer tokens, déjà câblé et testé en production. Tu modifies les couleurs de l'email et tu shippes.
Si tu veux comprendre chaque ligne avant d'utiliser, le tuto ci-dessus suffit. Si tu veux ship vite, le template est là.
Construire ton SaaS encore plus vite
ApexKit est le template full-stack TypeScript que j'ai construit pour livrer un SaaS en une semaine : auth passwordless, multi-tenant, billing Stripe, blog, docs et dashboard admin — câblés ensemble avec TanStack Start et Convex.
Web seul ou Web + mobile (Expo), source-of-truth Convex partagée, prêt à déployer sur VPS ou Vercel en quelques minutes.