Build This Now
Build This Now
Qu'est-ce que le code Claude ?Installer Claude CodeL'installateur natif de Claude CodeTon premier projet Claude Code
Failed Payment RecoveryLocalization and i18nPDF InvoicesReferral ProgramTwo-Factor AuthNext.js DevTools MCPNext.js Agent SetupSubscription BillingMulti-Tenant SaaSAI Chat FeatureRate LimitingStripe WebhooksSemantic SearchRealtime UpdatesDrip Email Sequencesv2.1.122 Release NotesClaude Code Dynamic Workflows : comment orchestrer 1 000 sous-agents sur une vraie codebaseBonnes pratiques Claude CodeMeilleures pratiques pour Claude Opus 4.7Claude Code sur un VPSIntégration GitRevue de code avec Claude CodeLes Worktrees avec Claude CodeClaude Code à distanceClaude Code ChannelsChannels, Routines, Teleport, DispatchTâches planifiées avec Claude CodePermissions Claude CodeLe mode auto de Claude CodeAjouter les paiements Stripe avec Claude CodeFeedback LoopsWorkflows TodoGestion des tâches dans Claude CodeTemplates de projetTarification et utilisation des tokens Claude CodeTarifs de Claude Code : ce que tu vas vraiment payerClaude Code Ultra ReviewConstruire une app Next.js avec Claude CodeSupabase DatabaseVercel DeepsecTest-Driven DevelopmentConstruire un MVP SaaS avec Claude CodeAjouter l'authentification avec Claude Code (Supabase Auth)Ajouter les emails transactionnels avec Claude Code (Resend + React Email)Construire une API type-safe avec Claude Code (oRPC + Zod)File UploadsBackground Jobs (Inngest)Admin DashboardFull-Text SearchClaude Agent SDKKiro Migration GuideClaude Research AgentLeave Grok BuildRoute Subagent ModelsClaude Monorepo SetupCI Repair AgentVisual Regression TestsAgent Cost DashboardCursor Migration GuideCommerce agentique : comment construire une app que les agents IA peuvent payer1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based BillingCombien coûte la création d'un SaaS avec Claude Code en 2026Parallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/Supabase Auth

Ajouter l'authentification avec Claude Code (Supabase Auth)

Ajoute l'inscription email/mot de passe, l'OAuth Google, les magic links, les routes protégées et la gestion des sessions à une app Next.js 16 avec Claude Code et Supabase Auth.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo
speedy_devvWritten by speedy_devvPublished Jul 14, 20269 min readHandbook hubWorkflow index

Supabase Auth te donne un système d'utilisateurs hébergé avec sessions, JWT et OAuth déjà construits. Tu n'écris pas de routine de hachage de mot de passe ni de boucle de rafraîchissement de token. Ce que tu écris, c'est la plomberie qui le relie à Next.js : deux clients Supabase, une étape de rafraîchissement de session dans proxy.ts, et les formulaires et Server Actions qui l'appellent. Claude Code peut tout générer correctement si tu lui donnes les bons patterns, et c'est justement ce que ce post détaille avec du code qui marche.

Ce que Supabase Auth te donne

Supabase Auth tourne par-dessus ta base Postgres. Les utilisateurs vivent dans un schéma appelé auth.users que tu ne requêtes jamais directement. Chaque inscription, connexion et flux OAuth se termine de la même façon : Supabase émet un JWT, et @supabase/ssr le stocke dans les cookies pour que ton serveur Next.js puisse le lire à chaque requête.

Tu obtiens l'auth email/mot de passe, les magic links (connexion sans mot de passe via un lien à usage unique envoyé par email) et l'OAuth avec plus de 30 fournisseurs prêts à l'emploi. Ce post prend Google comme exemple d'OAuth, puisque c'est celui vers lequel la plupart des builders se tournent en premier. Le pattern est le même pour n'importe quel autre fournisseur une fois que Google marche.

Installer les packages

Deux packages, tous deux de Supabase. @supabase/supabase-js est le client de base. @supabase/ssr ajoute la gestion de session basée sur les cookies, conçue pour les frameworks de rendu serveur comme Next.js.

npm install @supabase/supabase-js @supabase/ssr

Il te faut aussi deux variables d'environnement depuis le dashboard de ton projet Supabase, sous Project Settings > API.

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

La clé anon peut être exposée sans risque dans le navigateur. Le contrôle d'accès vient des policies de row-level security sur tes tables, pas du fait de cacher cette clé.

Deux clients : navigateur et serveur

Supabase Auth dans Next.js a besoin de deux clients distincts parce que les cookies fonctionnent différemment de chaque côté. Le client navigateur lit et écrit les cookies via document.cookie. Le client serveur les lit et les écrit via les objets de requête et de réponse que Next.js te donne.

Demande à Claude de créer d'abord le client navigateur. Il est court.

// utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Le client serveur est utilisé dans les Server Components, les Server Actions et les Route Handlers. Dans Next.js 16, cookies() de next/headers renvoie une Promise, il faut donc await.

// utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Called from a Server Component that can't set cookies.
            // proxy.ts refreshes the session instead, so this is safe to ignore.
          }
        },
      },
    }
  )
}

Ce try/catch n'est pas décoratif. Les Server Components ne peuvent pas du tout écrire de cookies, seulement les lire. L'étape de rafraîchissement de session dans proxy.ts (section suivante) est ce qui maintient vraiment la session en vie, donc un Server Component qui n'arrive pas à écrire un cookie ici est attendu, pas un bug.

Inscription et connexion email/mot de passe

Les Server Actions gèrent la soumission des formulaires. L'inscription et la connexion suivent la même forme : récupérer les champs du FormData, appeler la méthode Supabase, rediriger en cas de succès.

// app/login/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'

export async function signup(formData: FormData) {
  const supabase = await createClient()

  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const { error } = await supabase.auth.signUp({ email, password })

  if (error) {
    redirect(`/login?error=${encodeURIComponent(error.message)}`)
  }

  revalidatePath('/', 'layout')
  redirect('/check-email')
}

export async function login(formData: FormData) {
  const supabase = await createClient()

  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const { error } = await supabase.auth.signInWithPassword({ email, password })

  if (error) {
    redirect(`/login?error=${encodeURIComponent(error.message)}`)
  }

  revalidatePath('/', 'layout')
  redirect('/dashboard')
}

export async function signOut() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  revalidatePath('/', 'layout')
  redirect('/login')
}

Par défaut, Supabase exige une confirmation par email avant qu'un nouveau compte puisse se connecter, c'est pourquoi signup redirige vers une page /check-email au lieu d'aller directement au dashboard. Tu peux couper la confirmation dans le dashboard Supabase sous Authentication > Providers pour tester en local, mais laisse-la active en prod.

Le formulaire lui-même est un Client Component pour pouvoir afficher l'état de validation, mais l'action de soumission tourne sur le serveur.

// app/login/page.tsx
import { login, signup } from './actions'

export default function LoginPage({
  searchParams,
}: {
  searchParams: Promise<{ error?: string }>
}) {
  return (
    <form className="max-w-sm mx-auto py-12 space-y-4">
      <h1 className="text-2xl font-bold">Log in</h1>
      <input
        id="email"
        name="email"
        type="email"
        required
        placeholder="you@example.com"
        className="w-full border rounded px-3 py-2"
      />
      <input
        id="password"
        name="password"
        type="password"
        required
        placeholder="Password"
        className="w-full border rounded px-3 py-2"
      />
      <div className="flex gap-2">
        <button formAction={login} className="flex-1 bg-black text-white rounded py-2">
          Log in
        </button>
        <button formAction={signup} className="flex-1 border rounded py-2">
          Sign up
        </button>
      </div>
    </form>
  )
}

Demande à Claude d'ajouter l'affichage de searchParams.error et tu obtiens la gestion des messages d'erreur gratuitement. Comme searchParams est une Promise dans Next.js 16, le lire dans un Server Component a toujours besoin d'await si tu utilises la valeur directement dans le corps du composant.

Rafraîchir la session dans proxy.ts

Next.js 16 a renommé middleware.ts en proxy.ts, et la fonction exportée s'appelle désormais proxy au lieu de middleware. Claude écrit encore middleware.ts par défaut sans instruction de projet lui disant le contraire, alors ajoute une ligne à CLAUDE.md : « La logique de middleware va dans proxy.ts, exportée comme proxy, pas dans middleware.ts. »

La logique de rafraîchissement vit d'abord dans un fichier helper, puisqu'elle est plus facile à tester et à réutiliser.

// utils/supabase/proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const {
    data: { user },
  } = await supabase.auth.getUser()

  const protectedPaths = ['/dashboard', '/account']
  const isProtected = protectedPaths.some((path) => request.nextUrl.pathname.startsWith(path))

  if (!user && isProtected) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

Appeler getUser() ici fait deux choses à la fois : ça vérifie le token contre le serveur de Supabase, et ça rafraîchit le cookie de session si le token approche de son expiration. C'est pour ça que ça tourne à chaque requête et pas seulement sur les pages protégées, une session expirée doit se rafraîchir avant d'expirer, pas après.

Le fichier proxy.ts racine se contente d'appeler le helper et de définir les chemins sur lesquels il tourne.

// proxy.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/proxy'

export async function proxy(request: NextRequest) {
  return updateSession(request)
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Le matcher exclut les assets statiques et les images pour que le proxy ne lance pas de vérification de session sur chaque requête de favicon. Il tourne quand même sur chaque page et route API, ce qui garde les sessions fraîches dans toute l'app.

Protéger les routes (défense en profondeur)

La redirection du proxy ci-dessus couvre la plupart des cas, mais ne la traite pas comme la seule vérification. Un pattern de matcher est facile à mal régler subtilement, et s'il exclut en silence une route que tu voulais protéger, cette route est maintenant ouverte. Vérifie à nouveau dans la page.

// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const supabase = await createClient()
  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  return <>{children}</>
}

Ça coûte un aller-retour réseau supplémentaire vers Supabase par chargement de page protégée. C'est un juste échange contre le fait de ne pas dépendre d'un matcher regex comme unique frontière d'autorisation. Le vrai filet de sécurité sous ces deux vérifications, c'est la row-level security sur tes tables, couverte plus bas, puisque c'est la couche qu'un attaquant ne peut pas contourner.

OAuth Google

Active le fournisseur Google dans le dashboard Supabase sous Authentication > Providers avant d'écrire du code. Il te faut un Client ID et un Client Secret depuis la Google Cloud Console, et l'URI de redirection que Google doit accepter est celui que Supabase t'affiche sur cette page, de la forme https://your-project-ref.supabase.co/auth/v1/callback.

L'action de connexion demande une URL à Supabase et y redirige le navigateur.

// app/login/actions.ts (add to the same file)

export async function signInWithGoogle() {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })

  if (error || !data.url) {
    redirect('/login?error=Could not sign in with Google')
  }

  redirect(data.url)
}

NEXT_PUBLIC_SITE_URL doit être réglée sur ton URL réellement déployée (ou http://localhost:3000 en développement) pour que la redirection retombe sur ton app, pas sur celle par défaut de Supabase.

<form>
  <button formAction={signInWithGoogle} className="w-full border rounded py-2">
    Continue with Google
  </button>
</form>

Magic links

Un magic link est un lien de connexion à usage unique envoyé par email. Pas de mot de passe à définir ni à retenir. Supabase appelle ça un flux OTP sans mot de passe en coulisses, et il utilise la même route de callback que l'OAuth.

// app/login/actions.ts (add to the same file)

export async function sendMagicLink(formData: FormData) {
  const supabase = await createClient()
  const email = formData.get('email') as string

  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })

  if (error) {
    redirect(`/login?error=${encodeURIComponent(error.message)}`)
  }

  redirect('/check-email')
}

Branche-le à son propre petit formulaire puisqu'il n'a besoin que d'un champ email, pas de mot de passe.

<form className="space-y-2">
  <input name="email" type="email" required placeholder="you@example.com" className="w-full border rounded px-3 py-2" />
  <button formAction={sendMagicLink} className="w-full border rounded py-2">
    Send magic link
  </button>
</form>

La route de callback partagée par les deux flux

L'OAuth Google et les magic links se terminent tous deux par Supabase qui redirige le navigateur vers ton app avec un paramètre de requête code. Un seul Route Handler échange ce code contre une session.

// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/dashboard'

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }

  return NextResponse.redirect(`${origin}/login?error=Could not authenticate`)
}

C'est le seul endroit où exchangeCodeForSession est appelé. Chaque fournisseur OAuth et chaque magic link pointe ici, donc ajouter un second fournisseur plus tard ne demande pas de nouvelle route de callback, juste une nouvelle action de connexion qui redirige vers le même /auth/callback.

Row-level security calée sur auth.uid()

Rien dans le flux d'auth ci-dessus ne restreint ce qu'un utilisateur connecté peut voir dans ta base. C'est le boulot de Postgres, imposé via la row-level security (RLS). Un pattern courant est une table profiles qui reçoit automatiquement une ligne dès que quelqu'un s'inscrit, grâce à un trigger Postgres sur auth.users.

create table public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  email text,
  display_name text,
  created_at timestamptz default now()
);

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  insert into public.profiles (id, email)
  values (new.id, new.email);
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

Une fois la table en place, active la RLS et écris des policies qui vérifient le propriétaire de la ligne contre l'ID de l'utilisateur connecté.

alter table public.profiles enable row level security;

create policy "Users can view their own profile"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = id );

create policy "Users can update their own profile"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = id )
with check ( (select auth.uid()) = id );

auth.uid() lit l'ID utilisateur dans le JWT vérifié que @supabase/ssr attache à chaque requête. Il n'y a pas de vérification d'autorisation séparée à écrire dans ton code applicatif pour cette table, Postgres refuse la requête d'emblée si la ligne n'appartient pas à l'appelant. C'est la même garantie qui soutient la redirection de proxy.ts et la vérification de layout ci-dessus, sauf qu'elle ne peut pas être contournée par une vérification de route manquante. Pour le tableau complet de la RLS (policies INSERT/DELETE, vues, et le piège de security_invoker), voir Claude Code avec Supabase.

Demande à Claude de faire tourner tout ce flux de bout en bout une fois qu'il est branché : inscris-toi avec un nouvel email, confirme-le, connecte-toi, connecte-toi avec Google, demande un magic link, atteins une page protégée en étant déconnecté et confirme la redirection. C'est le vrai test qui compte, pas juste une vérification de types propre.

Chaque pièce ici (les deux clients, le rafraîchissement dans proxy.ts, la route de callback, la forme de la policy RLS) est quelque chose que Claude Code écrit correctement une fois, mais re-dérive de zéro sur chaque nouveau projet à moins que tu aies sauvegardé le pattern quelque part. Le Code Kit à $29 livre ça déjà branché : pages protégées, OAuth Google et RLS sur chaque table dès la première migration, pour qu'un nouveau projet démarre au-delà de cette plomberie au lieu de la reconstruire. C'est un moteur ponctuel posé sur Claude Code, pas un abonnement, et Claude Code lui-même a toujours besoin de son propre plan Anthropic payant en dessous.

Posté par @speedy_devv

Continue in Workflow

  • Commerce agentique : comment construire une app que les agents IA peuvent payer
    Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.
  • Bonnes pratiques Claude Code
    Cinq habitudes séparent les ingénieurs qui livrent avec Claude Code : les PRDs, les règles CLAUDE.md modulaires, les slash commands personnalisés, les resets /clear, et un état d'esprit d'évolution du système.
  • Le mode auto de Claude Code
    Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
  • Channels, Routines, Teleport, Dispatch
    Les quatre fonctionnalités Claude Code livrées par Anthropic en mars et avril 2026 qui transforment le CLI en une couche de coordination orientée événements, entre téléphone, web et desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Best SaaS Boilerplate 2026: The Honest Comparison
    An honest 2026 roundup of the best SaaS boilerplates and starter kits (ShipFast, Makerkit, Supastarter, SaaS Pegasus, Divjoy, open source), with real pricing, stacks, and the trade-off nobody mentions: a boilerplate still leaves you coding.
  • Changelog Claude Code
    Notes de version par version pour Claude Code, du bêta v0.2 à mars 2026. Bare mode, relais de permissions Channels, corrections OAuth, et chaque breaking change.
  • What It Really Costs to Build a SaaS MVP in 2026
    A buyer's cost breakdown for building a SaaS MVP in 2026. Real freelancer, agency, no-code, AI-tool, and DIY numbers with citations, plus where the money actually goes.
  • Templates de prompts qui livrent du code
    Dix recettes de prompts qui livrent du code : scaffolding full-stack, APIs, schémas, tests, refactors, debugging, reviews et CI. Chacun avec les modes d'échec à éviter.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo

Construire un MVP SaaS avec Claude Code

Un journal de build sur un week-end : monter une app Next.js 16, ajouter l'auth Supabase et Postgres, brancher la facturation Stripe, et déployer sur Vercel, le tout avec Claude Code.

Ajouter les emails transactionnels avec Claude Code (Resend + React Email)

Comment construire des emails de bienvenue, de reçu et de réinitialisation de mot de passe dans une app Next.js 16 avec Resend et React Email, en laissant Claude Code écrire les templates et la logique d'envoi.

On this page

Ce que Supabase Auth te donne
Installer les packages
Deux clients : navigateur et serveur
Inscription et connexion email/mot de passe
Rafraîchir la session dans proxy.ts
Protéger les routes (défense en profondeur)
OAuth Google
Magic links
La route de callback partagée par les deux flux
Row-level security calée sur auth.uid()

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →