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.

KIKillian
··6 min read
Cadenas numérique sur un fond bleuté symbolisant la sécurité d'authentification

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 :

  1. Better Auth core — gère sessions, OAuth providers, magic links, validation des tokens.

  2. Composant Convex — stocke les users + sessions dans la DB Convex, expose une API typée.

  3. 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(),
    ],
  },
});
Interface d'authentification moderne sur un smartphone avec un email saisi

É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/googlepas 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.

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.

Découvrir ApexKit →

Web seul ou Web + mobile (Expo), source-of-truth Convex partagée, prêt à déployer sur VPS ou Vercel en quelques minutes.

Testimonials

Command palette

Search and run actions.