Construire une application temps réel avec Supabase et Next.js 15 : guide complet

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Supabase est devenu l'alternative open-source de référence à Firebase, offrant un backend complet avec PostgreSQL, authentification, abonnements temps réel et stockage de fichiers — le tout prêt à l'emploi. Combiné avec Next.js 15 et l'App Router, vous obtenez un framework full-stack puissant qui gère le rendu côté serveur, les Server Actions et les interactions client de manière fluide.

Dans ce guide, vous allez construire un tableau de tâches collaboratif en temps réel — un mini-Trello où plusieurs utilisateurs peuvent ajouter, modifier et supprimer des tâches qui se synchronisent instantanément sur tous les navigateurs connectés. Vous apprendrez à maîtriser Supabase Auth, Row Level Security (RLS), les triggers de base de données et le système Realtime Broadcast.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé
  • Un compte Supabase — gratuit sur supabase.com
  • Des connaissances de base en React et TypeScript
  • Une familiarité avec le Next.js App Router (pages, layouts, Server Components)
  • Un éditeur de code (VS Code recommandé)

Ce que vous allez construire

Un tableau de tâches collaboratif avec les fonctionnalités suivantes :

  • Authentification par email/mot de passe avec routes protégées
  • Mises à jour des tâches en temps réel sur tous les clients connectés
  • Row Level Security garantissant que chaque utilisateur ne voit que ses propres données
  • Server Components pour le chargement initial des données
  • Server Actions pour les mutations
  • Middleware pour la gestion des sessions

Étape 1 : Créer un projet Supabase

Rendez-vous sur supabase.com/dashboard et créez un nouveau projet. Une fois votre projet prêt, notez ces valeurs depuis Settings > API :

  • Project URL — l'URL de votre instance Supabase
  • anon public — clé utilisable côté client
  • service_role — accès administrateur (gardez-la secrète)

Étape 2 : Configurer le schéma de base de données

Allez dans le SQL Editor de votre tableau de bord Supabase et exécutez le code suivant :

-- Créer une table profiles liée à auth.users
create table public.profiles (
  id uuid references auth.users on delete cascade not null primary key,
  email text,
  display_name text,
  created_at timestamptz default now() not null
);
 
-- Créer la table des tâches
create table public.tasks (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references auth.users on delete cascade not null,
  title text not null,
  description text,
  status text default 'todo' check (status in ('todo', 'in_progress', 'done')),
  created_at timestamptz default now() not null,
  updated_at timestamptz default now() not null
);
 
-- Activer Row Level Security
alter table public.profiles enable row level security;
alter table public.tasks enable row level security;
 
-- Profiles : les utilisateurs peuvent lire et modifier leur propre profil
create policy "Users can view own profile"
  on public.profiles for select
  using (auth.uid() = id);
 
create policy "Users can update own profile"
  on public.profiles for update
  using (auth.uid() = id);
 
-- Tasks : les utilisateurs peuvent gérer leurs propres tâches
create policy "Users can view own tasks"
  on public.tasks for select
  using (auth.uid() = user_id);
 
create policy "Users can create tasks"
  on public.tasks for insert
  with check (auth.uid() = user_id);
 
create policy "Users can update own tasks"
  on public.tasks for update
  using (auth.uid() = user_id);
 
create policy "Users can delete own tasks"
  on public.tasks for delete
  using (auth.uid() = user_id);
 
-- Créer automatiquement un profil à l'inscription
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, email, display_name)
  values (new.id, new.email, split_part(new.email, '@', 1));
  return new;
end;
$$ language plpgsql security definer;
 
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();
 
-- Mettre à jour updated_at automatiquement
create or replace function public.update_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;
 
create trigger tasks_updated_at
  before update on public.tasks
  for each row execute function public.update_updated_at();

Ce schéma met en place :

  • Une table profiles qui se remplit automatiquement à l'inscription
  • Une table tasks avec suivi de statut
  • Des politiques RLS pour que chaque utilisateur n'accède qu'à ses propres données
  • Des triggers pour les horodatages automatiques

Étape 3 : Configurer le Broadcast temps réel

Pour des mises à jour temps réel scalables, Supabase recommande le Broadcast via triggers de base de données plutôt que l'ancienne approche postgres_changes. Exécutez ceci dans le SQL Editor :

-- Trigger de broadcast pour la table tasks
create or replace function broadcast_task_changes()
returns trigger as $$
begin
  perform realtime.broadcast_changes(
    'tasks:updates',
    TG_OP,
    TG_OP,
    TG_TABLE_NAME,
    TG_TABLE_SCHEMA,
    NEW,
    OLD
  );
  return null;
end;
$$ language plpgsql security definer;
 
create trigger tasks_broadcast_trigger
  after insert or update or delete on public.tasks
  for each row execute function broadcast_task_changes();

Étape 4 : Initialiser le projet Next.js

npx create-next-app@latest task-board --typescript --tailwind --eslint --app --src-dir
cd task-board

Installez les packages Supabase :

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

Créez votre fichier .env.local :

NEXT_PUBLIC_SUPABASE_URL=https://votre-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=votre-cle-anon-ici

Étape 5 : Créer les clients Supabase

Vous avez besoin de deux clients : un pour le côté serveur (Server Components, Server Actions, Middleware) et un pour le navigateur.

Client navigateur

// src/lib/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!
  )
}

Client serveur

// src/lib/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 {
            // Appelé depuis un Server Component — on peut ignorer
            // si le middleware gère le rafraîchissement des sessions
          }
        },
      },
    }
  )
}

Étape 6 : Ajouter le Middleware pour la gestion des sessions

Le middleware rafraîchit la session utilisateur à chaque requête, empêchant l'expiration des tokens :

// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
 
export async function middleware(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()
 
  // Rediriger les utilisateurs non authentifiés vers la connexion
  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/signup') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }
 
  // Rediriger les utilisateurs authentifiés hors des pages d'auth
  if (user && (request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/signup')) {
    const url = request.nextUrl.clone()
    url.pathname = '/dashboard'
    return NextResponse.redirect(url)
  }
 
  return supabaseResponse
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Étape 7 : Construire les pages d'authentification

Page de connexion

// src/app/login/page.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()
 
  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)
 
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })
 
    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }
 
    router.push('/dashboard')
    router.refresh()
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <h2 className="text-3xl font-bold text-center text-gray-900">
          Connexion au Task Board
        </h2>
 
        <form onSubmit={handleLogin} className="space-y-6">
          {error && (
            <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
              {error}
            </div>
          )}
 
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Mot de passe
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <button
            type="submit"
            disabled={loading}
            className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? 'Connexion...' : 'Se connecter'}
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          Pas de compte ?{' '}
          <a href="/signup" className="text-blue-600 hover:underline">
            Créer un compte
          </a>
        </p>
      </div>
    </div>
  )
}

Page d'inscription

// src/app/signup/page.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export default function SignupPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()
 
  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)
 
    const { error } = await supabase.auth.signUp({
      email,
      password,
    })
 
    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }
 
    router.push('/dashboard')
    router.refresh()
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <h2 className="text-3xl font-bold text-center text-gray-900">
          Créer votre compte
        </h2>
 
        <form onSubmit={handleSignup} className="space-y-6">
          {error && (
            <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
              {error}
            </div>
          )}
 
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Mot de passe (min. 6 caractères)
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              minLength={6}
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <button
            type="submit"
            disabled={loading}
            className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? 'Création...' : 'Créer un compte'}
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          Déjà un compte ?{' '}
          <a href="/login" className="text-blue-600 hover:underline">
            Se connecter
          </a>
        </p>
      </div>
    </div>
  )
}

Étape 8 : Construire le Dashboard avec les Server Components

Le dashboard charge les tâches côté serveur pour un affichage initial rapide, puis s'abonne aux changements en temps réel côté client.

Layout du Dashboard

// src/app/dashboard/layout.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
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 (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-white shadow-sm border-b">
        <div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
          <h1 className="text-xl font-bold text-gray-900">Task Board</h1>
          <div className="flex items-center gap-4">
            <span className="text-sm text-gray-600">{user.email}</span>
            <form action="/auth/signout" method="post">
              <button
                type="submit"
                className="text-sm text-red-600 hover:underline"
              >
                Déconnexion
              </button>
            </form>
          </div>
        </div>
      </header>
      <main className="max-w-7xl mx-auto px-4 py-8">{children}</main>
    </div>
  )
}

Server Component : Charger les tâches initiales

// src/app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { TaskBoard } from './task-board'
 
export default async function DashboardPage() {
  const supabase = await createClient()
 
  const { data: tasks, error } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false })
 
  if (error) {
    return <div className="text-red-600">Erreur de chargement : {error.message}</div>
  }
 
  return <TaskBoard initialTasks={tasks ?? []} />
}

Étape 9 : Créer le composant Task Board temps réel

C'est le cœur de l'application — un composant client qui affiche les colonnes Kanban et s'abonne aux changements en temps réel :

// src/app/dashboard/task-board.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
 
type Task = {
  id: string
  title: string
  description: string | null
  status: 'todo' | 'in_progress' | 'done'
  created_at: string
  updated_at: string
  user_id: string
}
 
const STATUS_COLUMNS = [
  { key: 'todo', label: 'À faire', color: 'bg-yellow-100 border-yellow-300' },
  { key: 'in_progress', label: 'En cours', color: 'bg-blue-100 border-blue-300' },
  { key: 'done', label: 'Terminé', color: 'bg-green-100 border-green-300' },
] as const
 
export function TaskBoard({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState<Task[]>(initialTasks)
  const [newTitle, setNewTitle] = useState('')
  const supabase = createClient()
 
  // S'abonner aux changements en temps réel
  useEffect(() => {
    const channel = supabase
      .channel('tasks:updates', { config: { private: true } })
      .on('broadcast', { event: 'INSERT' }, (payload) => {
        const newTask = payload.payload.record as Task
        setTasks((prev) => [newTask, ...prev])
      })
      .on('broadcast', { event: 'UPDATE' }, (payload) => {
        const updated = payload.payload.record as Task
        setTasks((prev) =>
          prev.map((t) => (t.id === updated.id ? updated : t))
        )
      })
      .on('broadcast', { event: 'DELETE' }, (payload) => {
        const deleted = payload.payload.old_record as Task
        setTasks((prev) => prev.filter((t) => t.id !== deleted.id))
      })
      .subscribe()
 
    return () => {
      supabase.removeChannel(channel)
    }
  }, [supabase])
 
  const addTask = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!newTitle.trim()) return
 
    const { data: { user } } = await supabase.auth.getUser()
    if (!user) return
 
    const { error } = await supabase.from('tasks').insert({
      title: newTitle.trim(),
      user_id: user.id,
      status: 'todo',
    })
 
    if (!error) setNewTitle('')
  }
 
  const updateStatus = async (taskId: string, status: Task['status']) => {
    await supabase.from('tasks').update({ status }).eq('id', taskId)
  }
 
  const deleteTask = async (taskId: string) => {
    await supabase.from('tasks').delete().eq('id', taskId)
  }
 
  return (
    <div className="space-y-6">
      {/* Formulaire d'ajout */}
      <form onSubmit={addTask} className="flex gap-3">
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="Ajouter une nouvelle tâche..."
          className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          type="submit"
          className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
        >
          Ajouter
        </button>
      </form>
 
      {/* Colonnes Kanban */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {STATUS_COLUMNS.map((col) => (
          <div key={col.key} className={`rounded-xl border-2 p-4 ${col.color}`}>
            <h3 className="font-semibold text-lg mb-4">
              {col.label}
              <span className="ml-2 text-sm font-normal text-gray-500">
                ({tasks.filter((t) => t.status === col.key).length})
              </span>
            </h3>
 
            <div className="space-y-3">
              {tasks
                .filter((t) => t.status === col.key)
                .map((task) => (
                  <div
                    key={task.id}
                    className="bg-white rounded-lg p-4 shadow-sm border"
                  >
                    <h4 className="font-medium text-gray-900">{task.title}</h4>
                    {task.description && (
                      <p className="text-sm text-gray-500 mt-1">
                        {task.description}
                      </p>
                    )}
 
                    <div className="mt-3 flex gap-2 flex-wrap">
                      {col.key !== 'todo' && (
                        <button
                          onClick={() =>
                            updateStatus(
                              task.id,
                              col.key === 'done' ? 'in_progress' : 'todo'
                            )
                          }
                          className="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200"
                        >
                          ← Reculer
                        </button>
                      )}
                      {col.key !== 'done' && (
                        <button
                          onClick={() =>
                            updateStatus(
                              task.id,
                              col.key === 'todo' ? 'in_progress' : 'done'
                            )
                          }
                          className="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200 text-blue-700"
                        >
                          Avancer →
                        </button>
                      )}
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="text-xs px-2 py-1 bg-red-100 rounded hover:bg-red-200 text-red-700"
                      >
                        Supprimer
                      </button>
                    </div>
                  </div>
                ))}
 
              {tasks.filter((t) => t.status === col.key).length === 0 && (
                <p className="text-sm text-gray-400 text-center py-4">
                  Aucune tâche
                </p>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Étape 10 : Ajouter la route de déconnexion

// src/app/auth/signout/route.ts
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export async function POST() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/login')
}

Étape 11 : Gestionnaire de callback d'authentification

Supabase redirige les utilisateurs vers une URL de callback après la confirmation par email. Ajoutez cette route :

// src/app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
 
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
 
  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}/dashboard`)
    }
  }
 
  return NextResponse.redirect(`${origin}/login?error=auth`)
}

Tester votre implémentation

  1. Lancez le serveur de développement :
npm run dev
  1. Inscrivez-vous sur http://localhost:3000/signup avec un email de test
  2. Créez des tâches et déplacez-les entre les colonnes
  3. Ouvrez un second onglet — les changements apparaissent en temps réel
  4. Vérifiez le RLS — connectez-vous avec un autre compte pour vérifier l'isolation des tâches

Dépannage

Problèmes courants et solutions

"relation 'tasks' does not exist" — Assurez-vous d'avoir exécuté le SQL de l'étape 2 sur votre projet Supabase.

Les cookies ne sont pas définis — Vérifiez que votre middleware est configuré avec le bon pattern matcher et que @supabase/ssr est installé.

Les événements temps réel n'arrivent pas — Vérifiez que le trigger broadcast a été créé à l'étape 3. Consultez votre tableau de bord Supabase sous Realtime > Inspector pour voir si les événements circulent.

Erreurs "Unauthorized" — Row Level Security est activé. Assurez-vous que l'utilisateur est authentifié avant de faire des requêtes. Vérifiez que les politiques RLS font correspondre auth.uid() à la bonne colonne.

Session perdue au rafraîchissement — Le middleware devrait gérer le rafraîchissement automatiquement. Si les sessions sont perdues, vérifiez que setAll dans le client serveur écrit correctement les cookies.

Structure du projet

src/
├── app/
│   ├── auth/
│   │   ├── callback/route.ts    # Callback OAuth/email
│   │   └── signout/route.ts     # Gestionnaire de déconnexion
│   ├── dashboard/
│   │   ├── layout.tsx           # Protection auth + en-tête
│   │   ├── page.tsx             # Server Component (chargement)
│   │   └── task-board.tsx       # Client Component (temps réel)
│   ├── login/page.tsx           # Formulaire de connexion
│   └── signup/page.tsx          # Formulaire d'inscription
├── lib/
│   └── supabase/
│       ├── client.ts            # Client navigateur
│       └── server.ts            # Client serveur
└── middleware.ts                 # Rafraîchissement session + gardes

Prochaines étapes

Maintenant que vous avez un tableau de tâches temps réel fonctionnel, voici comment l'étendre :

  • Ajouter des fournisseurs OAuth — Activez la connexion Google, GitHub ou Discord via les paramètres Supabase Auth
  • Implémenter le drag-and-drop — Utilisez @dnd-kit/core pour des interactions Kanban fluides
  • Ajouter Supabase Storage — Permettez aux utilisateurs de joindre des fichiers aux tâches
  • Déployer sur Vercel — Configurez vos variables d'environnement et déployez avec vercel deploy
  • Ajouter la Presence — Montrez quels utilisateurs sont actuellement en ligne avec Supabase Realtime Presence

Conclusion

Vous avez construit un tableau de tâches collaboratif full-stack en temps réel avec Supabase et Next.js 15. Les patterns clés que vous avez appris sont :

  • Double configuration client — des clients Supabase séparés pour le navigateur et le serveur avec l'App Router
  • Gestion des sessions par middleware — rafraîchissement automatique des tokens à chaque requête
  • Row Level Security — contrôle d'accès au niveau de la base de données sans écrire de code backend
  • Triggers de broadcast — l'approche scalable pour les notifications de changements en temps réel
  • Server Components + Client Components — chargement de données côté serveur avec interactivité côté client

Ces patterns s'appliquent à n'importe quel projet Supabase + Next.js, que vous construisiez une application de chat, un dashboard ou un produit SaaS. Supabase gère l'infrastructure lourde pour que vous puissiez vous concentrer sur la logique de votre application.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Maîtriser la prise de notes avec FlutterFlow et Supabase : Un guide complet.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Starter Kit SaaS avec Next.js 15, Stripe et Auth.js v5

Apprenez a construire une application SaaS prete pour la production avec Next.js 15, Stripe pour la facturation par abonnement, et Auth.js v5 pour l'authentification. Ce tutoriel pas a pas couvre la configuration du projet, la connexion OAuth, les plans tarifaires, la gestion des webhooks et les routes protegees.

35 min read·