Construire un Agent IA Autonome avec Agentic RAG et Next.js

Les systemes RAG traditionnels suivent un schema lineaire : l'utilisateur pose une question, la base de donnees est interrogee, puis la reponse est generee. Mais que se passerait-il si vous vouliez un agent intelligent qui decide par lui-meme quand il a besoin de chercher, dans quelle source, et s'il a besoin de plus de contexte ?
C'est le Agentic RAG — le paradigme qui transforme l'IA d'un simple repondeur en un agent autonome qui pense, agit et apprend de ses resultats.
Pourquoi Agentic RAG en 2026 ? Avec l'adoption croissante des agents IA par les entreprises, le modele agentique est devenu la nouvelle norme. Au lieu de pipelines fixes, l'agent decide intelligemment quand utiliser chaque outil.
Ce que vous allez apprendre
A la fin de ce tutoriel, vous serez capable de :
- Comprendre la difference entre le RAG traditionnel et le Agentic RAG
- Construire un agent IA avec plusieurs outils en utilisant
ToolLoopAgent - Implementer une recherche vectorielle comme outil que l'agent invoque de maniere autonome
- Ajouter des outils de calcul et d'analyse que l'agent choisit d'utiliser
- Connecter le tout a une interface de chat dans Next.js
- Comprendre les patterns de conception d'agents pour les environnements de production
Quelle est la difference entre le RAG traditionnel et le Agentic RAG ?
RAG traditionnel
Question de l'utilisateur → Recherche dans la base de donnees → Generation de la reponse
Dans ce modele, une recherche est toujours effectuee quelle que soit la nature de la question. Si l'utilisateur demande "Bonjour, comment allez-vous ?" le systeme interrogera la base de donnees inutilement.
Agentic RAG
Question de l'utilisateur → L'agent reflechit → Decide : Ai-je besoin de chercher ?
↓ Oui ↓ Non
Cherche dans la Repond directement
source appropriee
↓
Ai-je besoin d'informations supplementaires ?
↓ Oui
Cherche a nouveau ou utilise un autre outil
↓
Genere la reponse finale
L'agent prend des decisions intelligentes a chaque etape :
- Quand chercher : Uniquement quand il a besoin d'informations externes
- Ou chercher : Il choisit la source appropriee parmi plusieurs sources
- Combien de fois chercher : Il peut effectuer plusieurs recherches consecutives
- Que faire des resultats : Il analyse, compare, calcule, puis repond
Prerequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installe sur votre machine
- Une cle API OpenAI avec acces aux modeles
gpt-4oettext-embedding-3-small - Des connaissances de base en TypeScript et React
- Une familiarite avec Next.js App Router
- Un editeur de code (VS Code recommande)
Note : Nous utiliserons une base de donnees vectorielle en memoire (in-memory) pour simplifier le tutoriel. En production, remplacez-la par Supabase pgvector, Pinecone ou toute autre base de donnees vectorielle.
Etape 1 : Configuration du projet
Commencez par creer un nouveau projet Next.js avec les dependances requises :
npx create-next-app@latest agentic-rag-demo --typescript --tailwind --app --src-dir
cd agentic-rag-demoInstallez les packages AI SDK et les utilitaires :
npm install ai @ai-sdk/openai zodCreez un fichier .env.local pour stocker votre cle API :
OPENAI_API_KEY=sk-your-api-key-hereEtape 2 : Construire la base de connaissances vectorielle
D'abord, nous allons creer une couche simple pour la base de donnees vectorielle. Creez le fichier src/lib/vector-store.ts :
import { embed, embedMany, cosineSimilarity } from "ai";
// Type de document stocke
interface Document {
id: string;
content: string;
embedding: number[];
metadata: {
source: string;
category: string;
};
}
// Base de donnees vectorielle en memoire
class VectorStore {
private documents: Document[] = [];
// Ajouter des documents avec generation d'embeddings
async addDocuments(
docs: { content: string; metadata: { source: string; category: string } }[]
) {
const { embeddings } = await embedMany({
model: "openai/text-embedding-3-small",
values: docs.map((d) => d.content),
});
const newDocs: Document[] = docs.map((doc, i) => ({
id: `doc-${Date.now()}-${i}`,
content: doc.content,
embedding: embeddings[i],
metadata: doc.metadata,
}));
this.documents.push(...newDocs);
return newDocs.length;
}
// Recherche semantique par similarite
async search(query: string, topK: number = 3): Promise<Document[]> {
const { embedding } = await embed({
model: "openai/text-embedding-3-small",
value: query,
});
const results = this.documents
.map((doc) => ({
...doc,
similarity: cosineSimilarity(embedding, doc.embedding),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
return results;
}
// Recherche avec filtrage par categorie
async searchByCategory(
query: string,
category: string,
topK: number = 3
): Promise<Document[]> {
const { embedding } = await embed({
model: "openai/text-embedding-3-small",
value: query,
});
const results = this.documents
.filter((doc) => doc.metadata.category === category)
.map((doc) => ({
...doc,
similarity: cosineSimilarity(embedding, doc.embedding),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
return results;
}
// Obtenir les categories disponibles
getCategories(): string[] {
return [...new Set(this.documents.map((d) => d.metadata.category))];
}
get size(): number {
return this.documents.length;
}
}
// Instance singleton partagee
export const vectorStore = new VectorStore();Etape 3 : Remplir la base de connaissances avec des donnees d'exemple
Creez le fichier src/lib/seed-data.ts pour remplir la base avec les donnees d'une entreprise fictive :
import { vectorStore } from "./vector-store";
export async function seedVectorStore() {
// Verifier que la base n'est pas deja remplie
if (vectorStore.size > 0) return;
const documents = [
// Politiques de l'entreprise
{
content:
"Politique de retour : Les clients peuvent retourner les produits dans les 30 jours suivant l'achat a condition que le produit soit dans son etat d'origine avec la facture. Le remboursement est effectue sous 5 a 7 jours ouvrables sur le mode de paiement original.",
metadata: { source: "company-policies", category: "politiques" },
},
{
content:
"Politique de livraison : Livraison gratuite pour les commandes de plus de 200 dinars. La livraison standard prend 3 a 5 jours ouvrables pour un cout de 7 dinars. La livraison express sous 24 heures est disponible pour 15 dinars.",
metadata: { source: "company-policies", category: "politiques" },
},
{
content:
"Politique de garantie : Tous les produits electroniques sont couverts par une garantie complete d'un an contre les defauts de fabrication. La garantie ne couvre pas les dommages resultant d'une mauvaise utilisation ou d'accidents.",
metadata: { source: "company-policies", category: "politiques" },
},
// Informations produits
{
content:
"Ordinateur portable ProBook X1 : Processeur Intel Core i7 14eme generation, 16 Go de RAM, 512 Go SSD, ecran FHD de 14 pouces. Prix : 2 500 dinars. Disponible en coloris : Argent, Gris fonce.",
metadata: { source: "product-catalog", category: "produits" },
},
{
content:
"Ordinateur portable ProBook X2 : Processeur Apple M3 Pro, 18 Go de RAM, 1 To SSD, ecran Liquid Retina de 16 pouces. Prix : 4 200 dinars. Disponible en coloris : Argent, Noir cosmique.",
metadata: { source: "product-catalog", category: "produits" },
},
{
content:
"Casque AirSound Pro : Reduction de bruit active, Bluetooth 5.3, autonomie de 30 heures, resistance a l'eau IPX5. Prix : 350 dinars.",
metadata: { source: "product-catalog", category: "produits" },
},
{
content:
"Ecran UltraView 27 : Ecran 27 pouces 4K HDR, taux de rafraichissement 144 Hz, support USB-C, pied ajustable. Prix : 1 800 dinars.",
metadata: { source: "product-catalog", category: "produits" },
},
// FAQ
{
content:
"Comment suivre ma commande ? Vous pouvez suivre le statut de votre commande via la page 'Mes commandes' dans votre compte, ou via le lien de suivi envoye a votre adresse e-mail lors de l'expedition.",
metadata: { source: "faq", category: "support" },
},
{
content:
"Modes de paiement disponibles : Carte bancaire (Visa, Mastercard), virement bancaire, paiement a la livraison (disponible uniquement dans le Grand Tunis), Flouci.",
metadata: { source: "faq", category: "support" },
},
{
content:
"Horaires du service client : Du lundi au vendredi, de 9h a 18h. Le samedi de 9h a 13h. Vous pouvez nous contacter par telephone, e-mail ou chat en direct.",
metadata: { source: "faq", category: "support" },
},
];
await vectorStore.addDocuments(documents);
console.log(`${vectorStore.size} documents charges dans la base de connaissances`);
}Etape 4 : Construire l'agent avec ses outils
Voici la partie la plus importante. Nous allons creer l'agent qui possede plusieurs outils et decide par lui-meme lesquels utiliser. Creez le fichier src/lib/agent.ts :
import { ToolLoopAgent, tool, streamText, generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { vectorStore } from "./vector-store";
// Outil 1 : Recherche dans la base de connaissances
const searchKnowledgeBase = tool({
description:
"Rechercher dans la base de connaissances de l'entreprise des informations sur les produits, les politiques et le support. Utilisez cet outil quand l'utilisateur pose une question sur un produit, une politique ou a besoin d'assistance technique.",
inputSchema: z.object({
query: z
.string()
.describe("Texte de recherche - une question ou des mots-cles a rechercher"),
category: z
.enum(["produits", "politiques", "support"])
.optional()
.describe("Categorie de recherche pour affiner les resultats"),
}),
execute: async ({ query, category }) => {
const results = category
? await vectorStore.searchByCategory(query, category)
: await vectorStore.search(query);
if (results.length === 0) {
return { found: false, message: "Aucun resultat correspondant trouve." };
}
return {
found: true,
results: results.map((r) => ({
content: r.content,
source: r.metadata.source,
category: r.metadata.category,
})),
};
},
});
// Outil 2 : Calculatrice
const calculator = tool({
description:
"Calculer des expressions mathematiques. Utilisez cet outil pour calculer les prix, les remises, les taxes et les frais de livraison.",
inputSchema: z.object({
expression: z.string().describe("L'expression mathematique a evaluer"),
context: z
.string()
.optional()
.describe("Contexte du calcul pour clarifier le resultat"),
}),
execute: async ({ expression, context }) => {
try {
// Calcul securise d'expressions mathematiques simples
const sanitized = expression.replace(/[^0-9+\-*/().% ]/g, "");
const result = Function(`"use strict"; return (${sanitized})`)();
return {
expression: sanitized,
result: Number(result.toFixed(2)),
context: context || "Resultat du calcul",
};
} catch {
return { error: "Expression mathematique invalide", expression };
}
},
});
// Outil 3 : Comparer les produits
const compareProducts = tool({
description:
"Comparer deux produits ou plus cote a cote. Utilisez cet outil quand l'utilisateur demande une comparaison entre des produits.",
inputSchema: z.object({
productNames: z
.array(z.string())
.min(2)
.describe("Noms des produits a comparer"),
}),
execute: async ({ productNames }) => {
const results = [];
for (const name of productNames) {
const searchResults = await vectorStore.searchByCategory(
name,
"produits",
1
);
if (searchResults.length > 0) {
results.push({
name,
details: searchResults[0].content,
});
}
}
return {
productsFound: results.length,
comparison: results,
};
},
});
// Outil 4 : Obtenir les categories disponibles
const getAvailableCategories = tool({
description:
"Obtenir la liste des categories disponibles dans la base de connaissances. Utilisez cet outil pour connaitre les types d'informations disponibles.",
inputSchema: z.object({}),
execute: async () => {
return {
categories: vectorStore.getCategories(),
totalDocuments: vectorStore.size,
};
},
});
// Definir les instructions de l'agent
const AGENT_INSTRUCTIONS = `Vous etes un assistant intelligent pour une entreprise technologique tunisienne. Vous communiquez en francais de maniere professionnelle et conviviale.
Regles de comportement :
1. Quand l'utilisateur pose une question sur un produit, une politique ou le support, utilisez l'outil de recherche dans la base de connaissances.
2. Si la question necessite des calculs (prix, remises, frais de livraison), utilisez la calculatrice.
3. Si une comparaison de produits est demandee, utilisez l'outil de comparaison.
4. Pour les questions generales comme les salutations ou les conversations courantes, repondez directement sans utiliser aucun outil.
5. Si vous ne trouvez pas suffisamment d'informations, dites-le honnêtement a l'utilisateur et suggerez de contacter le service client.
6. Vous pouvez utiliser plusieurs outils dans la meme conversation si necessaire.
7. Les prix sont en dinars tunisiens (TND).
Style de reponse :
- Soyez concis et utile
- Utilisez le formatage approprie (listes, numeros) pour organiser les informations
- Citez la source lorsque vous citez la base de connaissances`;
// Creer l'agent
export const supportAgent = new ToolLoopAgent({
model: openai("gpt-4o"),
system: AGENT_INSTRUCTIONS,
tools: {
searchKnowledgeBase,
calculator,
compareProducts,
getAvailableCategories,
},
});Remarquez la difference fondamentale : Dans le RAG traditionnel, vous interrogeriez la base de donnees a chaque message. Ici, l'agent decide intelligemment : cette question necessite-t-elle une recherche, ou puis-je repondre directement ?
Etape 5 : Construire le endpoint API
Creez le fichier src/app/api/chat/route.ts pour traiter les requetes :
import { supportAgent } from "@/lib/agent";
import { seedVectorStore } from "@/lib/seed-data";
// Remplir la base de connaissances a la premiere requete
let isSeeded = false;
export async function POST(req: Request) {
if (!isSeeded) {
await seedVectorStore();
isSeeded = true;
}
const { messages } = await req.json();
const result = await supportAgent.stream({
messages,
});
return result.toDataStreamResponse();
}Etape 6 : Construire l'interface de chat
Creez le fichier src/app/page.tsx pour l'interface utilisateur :
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat",
});
// Exemples de questions a essayer
const exampleQuestions = [
"Quelle est votre politique de retour ?",
"Comparez le ProBook X1 et le ProBook X2",
"Combien coutera le ProBook X1 avec la livraison express ?",
"Bonjour, comment allez-vous ?",
];
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* En-tete */}
<header className="bg-white border-b px-6 py-4">
<h1 className="text-xl font-bold text-gray-800">
🤖 Assistant Agentic RAG
</h1>
<p className="text-sm text-gray-500">
Un agent intelligent qui decide de maniere autonome quand et comment chercher des informations
</p>
</header>
{/* Zone des messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.length === 0 && (
<div className="text-center py-12">
<h2 className="text-lg font-semibold text-gray-600 mb-4">
Essayez l'une de ces questions :
</h2>
<div className="flex flex-wrap justify-center gap-2">
{exampleQuestions.map((q, i) => (
<button
key={i}
onClick={() => {
handleInputChange({
target: { value: q },
} as React.ChangeEvent<HTMLInputElement>);
}}
className="bg-white border rounded-full px-4 py-2 text-sm
text-gray-700 hover:bg-blue-50 hover:border-blue-300
transition-colors"
>
{q}
</button>
))}
</div>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-white border text-gray-800"
}`}
>
<div className="whitespace-pre-wrap">{message.content}</div>
{/* Afficher les invocations d'outils */}
{message.toolInvocations &&
message.toolInvocations.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-200">
{message.toolInvocations.map((tool, i) => (
<div
key={i}
className="text-xs text-gray-400 flex items-center gap-1"
>
<span>🔧</span>
<span>Utilise : {tool.toolName}</span>
</div>
))}
</div>
)}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border rounded-2xl px-4 py-3 text-gray-400">
Reflexion en cours...
</div>
</div>
)}
</div>
{/* Champ de saisie */}
<form
onSubmit={handleSubmit}
className="border-t bg-white p-4"
>
<div className="flex gap-2 max-w-4xl mx-auto">
<input
value={input}
onChange={handleInputChange}
placeholder="Tapez votre question ici..."
className="flex-1 border rounded-xl px-4 py-3 focus:outline-none
focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 text-white rounded-xl px-6 py-3
hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
Envoyer
</button>
</div>
</form>
</div>
);
}Etape 7 : Lancer l'application et la tester
Demarrez le serveur de developpement :
npm run devOuvrez votre navigateur sur http://localhost:3000 et testez ces scenarios :
Scenario 1 : Une question qui necessite une recherche
Utilisateur : Quelle est la politique de retour ?
Agent (utilise l'outil searchKnowledgeBase) : Vous pouvez retourner les produits dans les 30 jours suivant l'achat a condition que le produit soit dans son etat d'origine avec la facture...
Scenario 2 : Une question qui ne necessite pas de recherche
Utilisateur : Bonjour, comment allez-vous ?
Agent (repond directement sans outils) : Bonjour ! Je vais tres bien, merci de demander. Comment puis-je vous aider aujourd'hui ?
Scenario 3 : Une question qui necessite plusieurs outils
Utilisateur : Combien coutera le ProBook X1 avec une remise de 10 % et la livraison express ?
Agent (utilise searchKnowledgeBase puis calculator) :
- Prix du ProBook X1 : 2 500 dinars
- Remise (10 %) : -250 dinars
- Livraison express : +15 dinars
- Total : 2 265 dinars
Scenario 4 : Comparaison de produits
Utilisateur : Comparez le ProBook X1 et le ProBook X2
Agent (utilise compareProducts) : Fournit un tableau comparatif detaille...
Etape 8 : Patterns avances pour la production
Pattern 1 : Ajouter une memoire de conversation
En production, vous devez stocker le contexte de la conversation. Vous pouvez utiliser prepareCall pour injecter un contexte supplementaire :
const agentWithMemory = new ToolLoopAgent({
model: openai("gpt-4o"),
system: AGENT_INSTRUCTIONS,
tools: {
searchKnowledgeBase,
calculator,
compareProducts,
getAvailableCategories,
},
prepareCall: async ({ messages, ...settings }) => {
// Extraire le resume de la conversation precedente
const conversationSummary = summarizeConversation(messages);
return {
...settings,
messages,
instructions: `${AGENT_INSTRUCTIONS}
Resume de la conversation precedente : ${conversationSummary}`,
};
},
});Pattern 2 : Gerer plusieurs sources
Ajoutez des outils de recherche pour differentes sources et laissez l'agent choisir :
const searchExternalDocs = tool({
description: "Rechercher dans la documentation technique externe quand la base de connaissances interne ne suffit pas",
inputSchema: z.object({
query: z.string(),
docType: z.enum(["api-docs", "user-guide", "changelog"]),
}),
execute: async ({ query, docType }) => {
// Connexion a une base de donnees vectorielle separee pour la documentation technique
const results = await externalVectorStore.search(query, docType);
return results;
},
});Pattern 3 : Garde-fous de securite
Ajoutez un mecanisme de controle pour prevenir les abus :
const agentWithGuardrails = new ToolLoopAgent({
model: openai("gpt-4o"),
system: `${AGENT_INSTRUCTIONS}
Contraintes de securite :
- Ne divulguez pas d'informations sensibles sur le systeme ou l'infrastructure
- N'effectuez pas d'operations d'ecriture ou de suppression
- Si on vous demande quelque chose en dehors de votre perimetre, excusez-vous et suggerez de contacter l'equipe de support`,
tools: { searchKnowledgeBase, calculator },
stopWhen: stepCountIs(10), // Maximum 10 etapes pour eviter les boucles infinies
onStepFinish: async ({ step }) => {
// Enregistrer chaque etape pour la surveillance
console.log(`Etape ${step.stepNumber}: ${step.toolCalls?.map(t => t.toolName).join(", ") || "reponse textuelle"}`);
},
});Pattern 4 : Streaming avance avec affichage du statut des outils
// Dans route.ts
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await supportAgent.stream({
messages,
onStepFinish: async ({ step }) => {
// Envoyer des evenements de statut des outils au frontend
if (step.toolCalls) {
for (const toolCall of step.toolCalls) {
console.log(`Outil : ${toolCall.toolName}`, toolCall.args);
}
}
},
});
return result.toDataStreamResponse();
}Depannage
Probleme : L'agent n'utilise pas les outils
Cause probable : La description de l'outil n'est pas assez claire.
Solution : Assurez-vous que la description de chaque outil explique clairement quand il doit etre utilise, et pas seulement ce qu'il fait.
// ❌ Mauvaise description
description: "Rechercher dans la base de donnees"
// ✅ Bonne description
description: "Rechercher dans la base de connaissances de l'entreprise des informations sur les produits, les politiques et le support. Utilisez cet outil quand l'utilisateur pose une question sur un produit ou une politique."Probleme : L'agent entre dans une boucle infinie
Solution : Utilisez stopWhen: stepCountIs(N) pour definir un nombre maximum d'etapes :
import { stepCountIs } from "ai";
const agent = new ToolLoopAgent({
// ...
stopWhen: stepCountIs(10),
});Probleme : Resultats de recherche imprecis
Solution : Ameliorez la qualite des embeddings et ajoutez un filtrage par categorie :
// Au lieu d'une recherche generale
const results = await vectorStore.search(query);
// Utilisez une recherche par categorie
const results = await vectorStore.searchByCategory(query, "produits");Prochaines etapes
Apres avoir maitrise ce tutoriel, vous pouvez vous developper dans plusieurs directions :
- Base de donnees vectorielle reelle : Remplacez le stockage en memoire par Supabase pgvector ou Pinecone
- Systemes multi-agents : Creez un systeme de transfert entre agents specialises
- Protocole MCP : Convertissez vos outils en serveur MCP pour les partager avec Claude Desktop et Cursor
- Monitoring : Ajoutez le suivi des performances et des couts avec LangSmith ou Helicone
- Test des agents : Utilisez des frameworks d'evaluation comme TruLens pour mesurer la qualite des reponses
Conclusion
Dans ce tutoriel, nous avons construit un agent IA qui va au-dela du modele RAG traditionnel. Au lieu de chercher aveuglement a chaque requete, l'agent reflechit a chaque etape et decide :
- Ai-je besoin d'informations externes ? Si non, il repond directement
- Dans quelle source chercher ? Il choisit la categorie appropriee
- Ai-je besoin de calculs ? Il utilise la calculatrice
- Ai-je besoin d'une comparaison ? Il utilise l'outil de comparaison
- Les resultats sont-ils suffisants ? Si non, il cherche a nouveau
Ce pattern agentique est l'avenir des applications d'IA — des systemes qui pensent et agissent de maniere autonome au lieu de suivre des etapes predefinies.
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 des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.

Construire votre premier serveur MCP avec TypeScript : Outils, Ressources et Prompts
Apprenez a construire un serveur MCP pret pour la production en partant de zero avec TypeScript. Ce tutoriel pratique couvre les outils, les ressources, les prompts, le transport stdio, et la connexion a Claude Desktop et Cursor.

Construire un chatbot RAG avec Supabase pgvector et Next.js
Apprenez à construire un chatbot IA qui répond aux questions en utilisant vos propres données. Ce tutoriel couvre les embeddings vectoriels, la recherche sémantique et le RAG avec Supabase et Next.js.