Créer une Application Fullstack avec PocketBase et Next.js en 2026

PocketBase est un backend open source écrit en Go qui tient dans un seul fichier binaire. Il offre une base de données SQLite, une authentification intégrée, un stockage de fichiers, des abonnements temps réel et un tableau de bord d'administration — le tout sans configuration complexe. Combiné avec Next.js, il forme une stack fullstack moderne, légère et idéale pour les projets personnels, les MVP et les applications de taille moyenne.
Dans ce tutoriel, vous allez construire une application de gestion de tâches (todo app) complète avec authentification utilisateur, opérations CRUD temps réel et déploiement en production.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ installé sur votre machine
- npm ou pnpm comme gestionnaire de paquets
- Connaissances de base en React et TypeScript
- Un éditeur de code (VS Code recommandé)
- Un terminal (bash, zsh ou PowerShell)
Ce que vous allez construire
Une application de gestion de tâches avec les fonctionnalités suivantes :
- Inscription et connexion utilisateur
- Création, lecture, mise à jour et suppression de tâches
- Mise à jour en temps réel via les abonnements PocketBase
- Interface responsive avec Tailwind CSS
- Déploiement prêt pour la production
Étape 1 : Installer PocketBase
PocketBase est distribué sous forme d'un seul fichier exécutable. Téléchargez-le depuis le site officiel.
# Créer un dossier pour le backend
mkdir pocketbase-backend && cd pocketbase-backend
# Télécharger PocketBase (Linux/macOS)
# Visitez https://pocketbase.io/docs/ pour la dernière version
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
# Ou sur macOS avec Homebrew
brew install pocketbase
# Décompresser
unzip pocketbase_0.25.0_linux_amd64.zipLancez PocketBase :
./pocketbase serveVous verrez dans le terminal :
> Server started at: http://127.0.0.1:8090
> - REST API: http://127.0.0.1:8090/api/
> - Admin UI: http://127.0.0.1:8090/_/
Ouvrez http://127.0.0.1:8090/_/ dans votre navigateur pour accéder au tableau de bord d'administration. Lors du premier accès, créez un compte administrateur.
Étape 2 : Configurer les collections PocketBase
Dans le tableau de bord admin, créez une collection tasks avec les champs suivants :
| Champ | Type | Options |
|---|---|---|
title | Text | Requis, max 200 caractères |
description | Text | Optionnel |
completed | Bool | Valeur par défaut : false |
user | Relation | Collection : users, requis |
Configurer les règles d'accès
Dans l'onglet API Rules de la collection tasks :
- List/Search :
@request.auth.id != "" && user = @request.auth.id - View :
@request.auth.id != "" && user = @request.auth.id - Create :
@request.auth.id != "" - Update :
@request.auth.id != "" && user = @request.auth.id - Delete :
@request.auth.id != "" && user = @request.auth.id
Ces règles garantissent que chaque utilisateur ne peut voir et modifier que ses propres tâches.
Étape 3 : Créer le projet Next.js
Ouvrez un nouveau terminal et créez le projet frontend :
npx create-next-app@latest pocketbase-todo --typescript --tailwind --app --src-dir --use-npm
cd pocketbase-todoInstallez le SDK PocketBase :
npm install pocketbaseÉtape 4 : Configurer le client PocketBase
Créez le fichier de configuration du client PocketBase :
// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
// Désactiver l'auto-annulation pour éviter les conflits avec React
pb.autoCancellation(false);
export default pb;Créez les types TypeScript pour vos données :
// src/types/index.ts
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
user: string;
created: string;
updated: string;
}
export interface User {
id: string;
email: string;
name: string;
avatar: string;
}Étape 5 : Créer le contexte d'authentification
Créez un provider React pour gérer l'état d'authentification globalement :
// src/contexts/AuthContext.tsx
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import pb from "@/lib/pocketbase";
import type { User } from "@/types";
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Vérifier si l'utilisateur est déjà connecté
if (pb.authStore.isValid) {
const model = pb.authStore.model;
if (model) {
setUser({
id: model.id,
email: model.email,
name: model.name || "",
avatar: model.avatar || "",
});
}
}
setIsLoading(false);
// Écouter les changements d'authentification
const unsubscribe = pb.authStore.onChange((_token, model) => {
if (model) {
setUser({
id: model.id,
email: model.email,
name: model.name || "",
avatar: model.avatar || "",
});
} else {
setUser(null);
}
});
return () => unsubscribe();
}, []);
const login = useCallback(async (email: string, password: string) => {
await pb.collection("users").authWithPassword(email, password);
}, []);
const register = useCallback(
async (email: string, password: string, name: string) => {
await pb.collection("users").create({
email,
password,
passwordConfirm: password,
name,
});
// Connexion automatique après inscription
await pb.collection("users").authWithPassword(email, password);
},
[]
);
const logout = useCallback(() => {
pb.authStore.clear();
setUser(null);
}, []);
return (
<AuthContext.Provider value={{ user, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}Étape 6 : Créer le hook de gestion des tâches
Ce hook personnalisé encapsule toute la logique CRUD et les abonnements temps réel :
// src/hooks/useTasks.ts
"use client";
import { useState, useEffect, useCallback } from "react";
import pb from "@/lib/pocketbase";
import type { Task } from "@/types";
import { useAuth } from "@/contexts/AuthContext";
export function useTasks() {
const [tasks, setTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { user } = useAuth();
// Charger les tâches
const fetchTasks = useCallback(async () => {
if (!user) return;
try {
setIsLoading(true);
const records = await pb.collection("tasks").getFullList<Task>({
sort: "-created",
filter: `user = "${user.id}"`,
});
setTasks(records);
} catch (error) {
console.error("Erreur lors du chargement des tâches:", error);
} finally {
setIsLoading(false);
}
}, [user]);
// Abonnement temps réel
useEffect(() => {
if (!user) return;
fetchTasks();
// S'abonner aux changements de la collection
pb.collection("tasks").subscribe<Task>("*", (event) => {
switch (event.action) {
case "create":
setTasks((prev) => [event.record, ...prev]);
break;
case "update":
setTasks((prev) =>
prev.map((task) =>
task.id === event.record.id ? event.record : task
)
);
break;
case "delete":
setTasks((prev) =>
prev.filter((task) => task.id !== event.record.id)
);
break;
}
});
return () => {
pb.collection("tasks").unsubscribe("*");
};
}, [user, fetchTasks]);
// Créer une tâche
const createTask = useCallback(
async (title: string, description: string = "") => {
if (!user) return;
await pb.collection("tasks").create({
title,
description,
completed: false,
user: user.id,
});
},
[user]
);
// Basculer l'état d'une tâche
const toggleTask = useCallback(async (task: Task) => {
await pb.collection("tasks").update(task.id, {
completed: !task.completed,
});
}, []);
// Supprimer une tâche
const deleteTask = useCallback(async (taskId: string) => {
await pb.collection("tasks").delete(taskId);
}, []);
// Mettre à jour une tâche
const updateTask = useCallback(
async (taskId: string, data: Partial<Task>) => {
await pb.collection("tasks").update(taskId, data);
},
[]
);
return {
tasks,
isLoading,
createTask,
toggleTask,
deleteTask,
updateTask,
refetch: fetchTasks,
};
}Étape 7 : Construire les composants UI
Formulaire de connexion/inscription
// src/components/AuthForm.tsx
"use client";
import { useState, type FormEvent } from "react";
import { useAuth } from "@/contexts/AuthContext";
export default function AuthForm() {
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const { login, register } = useAuth();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
setIsSubmitting(true);
try {
if (isLogin) {
await login(email, password);
} else {
await register(email, password, name);
}
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Une erreur est survenue";
setError(message);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="mx-auto max-w-md rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-6 text-center text-2xl font-bold text-gray-800">
{isLogin ? "Connexion" : "Inscription"}
</h2>
{error && (
<div className="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{!isLogin && (
<input
type="text"
placeholder="Votre nom"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
/>
)}
<input
type="email"
placeholder="Adresse e-mail"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
/>
<input
type="password"
placeholder="Mot de passe"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
minLength={8}
/>
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-lg bg-blue-600 py-3 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting
? "Chargement..."
: isLogin
? "Se connecter"
: "Créer un compte"}
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
{isLogin ? "Pas encore de compte ?" : "Déjà un compte ?"}
<button
onClick={() => setIsLogin(!isLogin)}
className="ml-1 font-medium text-blue-600 hover:underline"
>
{isLogin ? "S'inscrire" : "Se connecter"}
</button>
</p>
</div>
);
}Composant de liste de tâches
// src/components/TaskList.tsx
"use client";
import { useState, type FormEvent } from "react";
import { useTasks } from "@/hooks/useTasks";
import { useAuth } from "@/contexts/AuthContext";
import type { Task } from "@/types";
function TaskItem({
task,
onToggle,
onDelete,
}: {
task: Task;
onToggle: (task: Task) => void;
onDelete: (id: string) => void;
}) {
return (
<div className="group flex items-center gap-3 rounded-lg border bg-white p-4 transition hover:shadow-md">
<button
onClick={() => onToggle(task)}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 transition ${
task.completed
? "border-green-500 bg-green-500 text-white"
: "border-gray-300 hover:border-blue-400"
}`}
>
{task.completed && (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<div className="flex-1">
<h3
className={`font-medium ${
task.completed ? "text-gray-400 line-through" : "text-gray-800"
}`}
>
{task.title}
</h3>
{task.description && (
<p className="mt-1 text-sm text-gray-500">{task.description}</p>
)}
</div>
<button
onClick={() => onDelete(task.id)}
className="rounded-lg p-2 text-gray-400 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
);
}
export default function TaskList() {
const [newTitle, setNewTitle] = useState("");
const [newDescription, setNewDescription] = useState("");
const { tasks, isLoading, createTask, toggleTask, deleteTask } = useTasks();
const { user, logout } = useAuth();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
await createTask(newTitle.trim(), newDescription.trim());
setNewTitle("");
setNewDescription("");
};
const completedCount = tasks.filter((t) => t.completed).length;
return (
<div className="mx-auto max-w-2xl">
{/* En-tête */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-800">Mes Tâches</h1>
<p className="mt-1 text-gray-500">
Bonjour, {user?.name || user?.email}
</p>
</div>
<button
onClick={logout}
className="rounded-lg px-4 py-2 text-sm text-gray-600 transition hover:bg-gray-100"
>
Déconnexion
</button>
</div>
{/* Formulaire d'ajout */}
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
<input
type="text"
placeholder="Ajouter une nouvelle tâche..."
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="w-full rounded-xl border-2 border-gray-200 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none"
/>
<div className="flex gap-3">
<input
type="text"
placeholder="Description (optionnel)"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
className="flex-1 rounded-lg border px-4 py-2 focus:border-blue-500 focus:outline-none"
/>
<button
type="submit"
disabled={!newTitle.trim()}
className="rounded-lg bg-blue-600 px-6 py-2 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
>
Ajouter
</button>
</div>
</form>
{/* Statistiques */}
<div className="mb-4 flex gap-4 text-sm text-gray-500">
<span>{tasks.length} tâche(s) au total</span>
<span>{completedCount} terminée(s)</span>
<span>{tasks.length - completedCount} en cours</span>
</div>
{/* Liste des tâches */}
{isLoading ? (
<div className="py-12 text-center text-gray-400">
Chargement des tâches...
</div>
) : tasks.length === 0 ? (
<div className="py-12 text-center text-gray-400">
<p className="text-lg">Aucune tâche pour le moment</p>
<p className="mt-2 text-sm">
Créez votre première tâche ci-dessus
</p>
</div>
) : (
<div className="space-y-2">
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))}
</div>
)}
</div>
);
}Étape 8 : Assembler la page principale
Intégrez le provider d'authentification dans le layout :
// src/app/layout.tsx
import type { Metadata } from "next";
import { AuthProvider } from "@/contexts/AuthContext";
import "./globals.css";
export const metadata: Metadata = {
title: "Todo App - PocketBase + Next.js",
description: "Application de gestion de tâches avec PocketBase et Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}Créez la page principale qui affiche soit le formulaire d'authentification, soit la liste des tâches :
// src/app/page.tsx
"use client";
import AuthForm from "@/components/AuthForm";
import TaskList from "@/components/TaskList";
import { useAuth } from "@/contexts/AuthContext";
export default function Home() {
const { user, isLoading } = useAuth();
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
</div>
);
}
return (
<main className="min-h-screen bg-gray-50 px-4 py-12">
{user ? <TaskList /> : <AuthForm />}
</main>
);
}Étape 9 : Variables d'environnement
Créez un fichier .env.local pour configurer l'URL de PocketBase :
NEXT_PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090Mettez à jour le client PocketBase pour utiliser cette variable :
// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
const pb = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"
);
pb.autoCancellation(false);
export default pb;Étape 10 : Lancer l'application
Ouvrez deux terminaux :
# Terminal 1 : PocketBase
cd pocketbase-backend
./pocketbase serve
# Terminal 2 : Next.js
cd pocketbase-todo
npm run devOuvrez http://localhost:3000 dans votre navigateur. Vous devriez voir le formulaire de connexion. Créez un compte, puis commencez à ajouter des tâches.
Fonctionnalités avancées
Filtrage et recherche
Ajoutez un système de filtrage à votre liste de tâches :
// Dans useTasks.ts, ajoutez une fonction de recherche
const searchTasks = useCallback(
async (query: string) => {
if (!user) return;
const records = await pb.collection("tasks").getFullList<Task>({
sort: "-created",
filter: `user = "${user.id}" && title ~ "${query}"`,
});
setTasks(records);
},
[user]
);Pagination
Pour les applications avec beaucoup de données, utilisez la pagination :
const fetchTasksPaginated = useCallback(
async (page: number = 1, perPage: number = 20) => {
if (!user) return;
const result = await pb.collection("tasks").getList<Task>(page, perPage, {
sort: "-created",
filter: `user = "${user.id}"`,
});
return {
items: result.items,
totalPages: result.totalPages,
totalItems: result.totalItems,
};
},
[user]
);Upload de fichiers
PocketBase gère nativement les fichiers. Voici comment ajouter des pièces jointes aux tâches :
const createTaskWithFile = useCallback(
async (title: string, file: File) => {
if (!user) return;
const formData = new FormData();
formData.append("title", title);
formData.append("user", user.id);
formData.append("completed", "false");
formData.append("attachment", file);
await pb.collection("tasks").create(formData);
},
[user]
);Déploiement en production
Déployer PocketBase
PocketBase peut être déployé sur n'importe quel serveur Linux :
# Sur le serveur
mkdir -p /opt/pocketbase
cd /opt/pocketbase
# Télécharger et extraire PocketBase
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
unzip pocketbase_0.25.0_linux_amd64.zip
# Créer un service systemd
sudo tee /etc/systemd/system/pocketbase.service > /dev/null << 'EOF'
[Unit]
Description=PocketBase
After=network.target
[Service]
Type=simple
User=root
ExecStart=/opt/pocketbase/pocketbase serve --http="0.0.0.0:8090"
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
# Activer et démarrer le service
sudo systemctl enable pocketbase
sudo systemctl start pocketbaseDéployer Next.js
Déployez le frontend sur Vercel, Netlify ou votre propre serveur :
# Mettre à jour l'URL PocketBase pour la production
# .env.production
NEXT_PUBLIC_POCKETBASE_URL=https://api.votre-domaine.com
# Build de production
npm run build
npm startConfiguration Nginx (reverse proxy)
server {
listen 80;
server_name api.votre-domaine.com;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support pour le temps réel
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Dépannage
Erreur CORS
Si vous rencontrez des erreurs CORS, lancez PocketBase avec l'option origins :
./pocketbase serve --origins="http://localhost:3000,https://votre-domaine.com"Erreur de connexion au WebSocket
Assurez-vous que votre reverse proxy est configuré pour supporter les WebSockets (voir la configuration Nginx ci-dessus avec les en-têtes Upgrade et Connection).
Les données ne se mettent pas à jour en temps réel
Vérifiez que :
- Le client PocketBase est configuré avec
autoCancellation(false) - L'abonnement (
subscribe) est correctement initialisé - Le nettoyage (
unsubscribe) est fait dans le return duuseEffect
Prochaines étapes
Maintenant que votre application fonctionne, voici quelques idées pour aller plus loin :
- Ajouter des catégories : créez une collection "categories" et liez-la aux tâches
- Implémenter le drag-and-drop : utilisez
@dnd-kit/corepour réordonner les tâches - Ajouter des notifications : envoyez des rappels par email via les hooks PocketBase
- Mode hors ligne : utilisez un service worker pour permettre l'utilisation sans connexion
- Tests E2E : ajoutez des tests avec Playwright pour valider les flux utilisateur
Conclusion
Vous avez construit une application fullstack complète avec PocketBase et Next.js. PocketBase offre une alternative légère et puissante aux backends traditionnels, avec des fonctionnalités comme l'authentification, le temps réel et le stockage de fichiers — le tout dans un seul binaire.
La combinaison PocketBase + Next.js est particulièrement adaptée pour :
- Les prototypes rapides et les MVP
- Les applications personnelles et les side projects
- Les petites et moyennes applications en production
- Les développeurs souhaitant contrôler entièrement leur stack
Le code source complet de ce tutoriel est disponible pour référence et peut être adapté à vos propres projets.
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 une application full-stack en temps réel avec Convex et Next.js 15
Apprenez à construire une application full-stack en temps réel avec Convex et Next.js 15. Ce tutoriel couvre la conception de schémas, les requêtes, les mutations, les abonnements en temps réel, l'authentification et le téléchargement de fichiers — le tout avec une sécurité de types de bout en bout.

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Chatbot IA Local avec Ollama et Next.js : Guide Complet
Construisez un chatbot IA privé fonctionnant entièrement sur votre machine locale avec Ollama et Next.js. Ce tutoriel pratique couvre l'installation, le streaming des réponses, la sélection de modèles et le déploiement d'une interface de chat prête pour la production — le tout sans envoyer de données dans le cloud.