Construire une Application Full-Stack avec Appwrite Cloud et Next.js 15

Appwrite est un puissant backend-as-a-service (BaaS) open source qui fournit authentification, bases de données, stockage de fichiers, fonctions cloud et capacités temps réel prêts à l'emploi. Contrairement aux alternatives propriétaires, Appwrite vous donne un contrôle total — vous pouvez l'auto-héberger ou utiliser Appwrite Cloud pour une expérience gérée. Combiné avec Next.js 15 et l'App Router, vous obtenez une architecture full-stack moderne et typée qui gère à la fois le rendu côté serveur et l'interactivité côté client.
Dans ce tutoriel, vous allez construire un gestionnaire de favoris — une application full-stack où les utilisateurs peuvent sauvegarder, organiser, taguer et partager des favoris. Vous implémenterez l'authentification OAuth, une base de données documentaire pour stocker les favoris, le stockage de fichiers pour les captures d'écran, et les mises à jour en temps réel lorsque des favoris sont ajoutés ou modifiés.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un compte Appwrite Cloud — niveau gratuit disponible sur cloud.appwrite.io
- Des connaissances de base en React et TypeScript
- Une familiarité avec Next.js App Router (pages, layouts, composants serveur)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez construire
Une application de gestion de favoris avec ces fonctionnalités :
- Authentification OAuth (Google, GitHub) avec gestion des sessions
- Base de données documentaire pour stocker les favoris avec collections et attributs
- Stockage de fichiers pour les vignettes et favicons des favoris
- Abonnements temps réel pour les mises à jour en direct entre les onglets
- Composants Serveur pour le chargement initial optimisé SEO
- Actions Serveur pour les mutations sécurisées
- Organisation par tags avec filtrage et recherche
Étape 1 : Créer un projet Appwrite Cloud
Rendez-vous sur cloud.appwrite.io et créez un nouveau projet :
- Cliquez sur Create Project
- Entrez un nom comme "Bookmark Manager"
- Sélectionnez votre région préférée
- Une fois créé, notez votre Project ID depuis les paramètres du projet
Ensuite, configurez votre plateforme. Allez dans Overview et ajoutez une plateforme Web :
- Nom : Bookmark Manager Web
- Hostname :
localhost(pour le développement)
Cela enregistre votre application web et lui permet de communiquer avec les API Appwrite.
Étape 2 : Configurer le projet Next.js
Créez une nouvelle application Next.js 15 avec TypeScript et Tailwind CSS :
npx create-next-app@latest bookmark-manager --typescript --tailwind --app --src-dir --eslint
cd bookmark-managerInstallez le SDK Appwrite :
npm install appwrite node-appwriteLe package appwrite est pour l'utilisation côté client, tandis que node-appwrite est le SDK côté serveur pour les Composants Serveur et les Actions Serveur.
Étape 3 : Configurer les variables d'environnement
Créez un fichier .env.local à la racine du projet :
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-api-keyLe préfixe NEXT_PUBLIC_ rend les variables accessibles côté client. APPWRITE_API_KEY est réservée au serveur et ne doit jamais être exposée au navigateur.
Pour générer une clé API, allez dans votre Console Appwrite, naviguez vers Overview > API Keys, et créez une clé avec les portées suivantes :
databases.read,databases.writecollections.read,collections.writedocuments.read,documents.writefiles.read,files.writeusers.read
Étape 4 : Créer les configurations du SDK Appwrite
Créez deux configurations SDK — une pour le client et une pour le serveur.
SDK Client
// src/lib/appwrite/client.ts
import { Client, Account, Databases, Storage } from "appwrite";
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);
export const account = new Account(client);
export const databases = new Databases(client);
export const storage = new Storage(client);
export { client };SDK Serveur
// src/lib/appwrite/server.ts
import { Client, Databases, Storage, Users } from "node-appwrite";
export function createAdminClient() {
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
.setKey(process.env.APPWRITE_API_KEY!);
return {
databases: new Databases(client),
storage: new Storage(client),
users: new Users(client),
};
}
export function createSessionClient(session: string) {
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
.setSession(session);
return {
account: new Account(client),
databases: new Databases(client),
};
}createAdminClient utilise une clé API pour un accès complet, tandis que createSessionClient utilise la session utilisateur pour des requêtes authentifiées qui respectent les permissions.
Étape 5 : Configurer le schéma de la base de données
Dans la Console Appwrite, naviguez vers Databases et créez une nouvelle base de données appelée bookmark_db. Ensuite, créez les collections suivantes :
Collection des favoris
Créez une collection nommée bookmarks avec ces attributs :
| Attribut | Type | Requis | Description |
|---|---|---|---|
url | String (2048) | Oui | URL du favori |
title | String (256) | Oui | Titre de la page |
description | String (1024) | Non | Description de la page |
tags | String[] (50) | Non | Tableau de tags |
thumbnailId | String (36) | Non | ID du fichier de vignette |
favicon | String (2048) | Non | URL du favicon |
userId | String (36) | Oui | ID de l'utilisateur propriétaire |
isPublic | Boolean | Oui | Si le favori est visible publiquement |
createdAt | DateTime | Oui | Horodatage de création |
Créez des index pour des requêtes efficaces :
- Index sur
userId— type : Key - Index sur
tags— type : Key (index de tableau) - Index sur
createdAt— type : Key, ordre : DESC
Permissions de la collection
Définissez les permissions sur la collection bookmarks :
- Tous — Lecture (pour les favoris publics)
- Utilisateurs — Créer, Lire, Mettre à jour, Supprimer
Nous ajouterons des permissions au niveau du document pour contrôler l'accès par favori.
Définissons les constantes pour les identifiants de base de données et de collection :
// src/lib/appwrite/config.ts
export const DATABASE_ID = "bookmark_db";
export const BOOKMARKS_COLLECTION_ID = "bookmarks";
export const THUMBNAILS_BUCKET_ID = "thumbnails";Étape 6 : Implémenter l'authentification
Appwrite prend en charge l'authentification par email/mot de passe, OAuth, téléphone et lien magique. Implémentons l'OAuth Google et GitHub.
Configurer les fournisseurs OAuth
Dans la Console Appwrite, allez dans Auth > Settings et activez :
- Google — Ajoutez votre client ID et secret Google Cloud OAuth 2.0
- GitHub — Ajoutez votre client ID et secret GitHub OAuth App
Définissez l'URL de redirection à http://localhost:3000/auth/callback pour les deux fournisseurs.
Contexte d'authentification
// src/lib/auth/context.tsx
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { account } from "@/lib/appwrite/client";
import { Models } from "appwrite";
type AuthContextType = {
user: Models.User<Models.Preferences> | null;
loading: boolean;
login: (provider: "google" | "github") => void;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkUser();
}, []);
async function checkUser() {
try {
const currentUser = await account.get();
setUser(currentUser);
} catch {
setUser(null);
} finally {
setLoading(false);
}
}
function login(provider: "google" | "github") {
const redirectUrl = `${window.location.origin}/auth/callback`;
const failureUrl = `${window.location.origin}/auth/failure`;
account.createOAuth2Session(provider, redirectUrl, failureUrl);
}
async function logout() {
await account.deleteSession("current");
setUser(null);
}
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
}Page de callback d'authentification
// src/app/auth/callback/page.tsx
import { redirect } from "next/navigation";
export default function AuthCallback() {
redirect("/dashboard");
}Page de connexion
// src/app/login/page.tsx
"use client";
import { useAuth } from "@/lib/auth/context";
export default function LoginPage() {
const { login, loading, user } = useAuth();
if (loading) return <div className="flex justify-center p-8">Chargement...</div>;
if (user) return redirect("/dashboard");
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">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">Gestionnaire de Favoris</h1>
<p className="mt-2 text-gray-600">Connectez-vous pour gérer vos favoris</p>
</div>
<div className="space-y-4">
<button
onClick={() => login("google")}
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
<span>Continuer avec Google</span>
</button>
<button
onClick={() => login("github")}
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition"
>
<span>Continuer avec GitHub</span>
</button>
</div>
</div>
</div>
);
}Étape 7 : Construire les opérations CRUD des favoris
Créez une couche de services pour les opérations sur les favoris avec les Actions Serveur :
// src/lib/actions/bookmarks.ts
"use server";
import { createAdminClient } from "@/lib/appwrite/server";
import { DATABASE_ID, BOOKMARKS_COLLECTION_ID } from "@/lib/appwrite/config";
import { ID, Query } from "node-appwrite";
import { revalidatePath } from "next/cache";
export type Bookmark = {
$id: string;
url: string;
title: string;
description: string;
tags: string[];
thumbnailId: string | null;
favicon: string;
userId: string;
isPublic: boolean;
createdAt: string;
};
export async function createBookmark(formData: FormData) {
const { databases } = createAdminClient();
const url = formData.get("url") as string;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const tags = (formData.get("tags") as string)
.split(",")
.map((t) => t.trim())
.filter(Boolean);
const userId = formData.get("userId") as string;
const isPublic = formData.get("isPublic") === "true";
const bookmark = await databases.createDocument(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
ID.unique(),
{
url,
title,
description,
tags,
userId,
isPublic,
favicon: `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`,
createdAt: new Date().toISOString(),
}
);
revalidatePath("/dashboard");
return bookmark;
}
export async function getBookmarks(userId: string) {
const { databases } = createAdminClient();
const response = await databases.listDocuments(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
[
Query.equal("userId", userId),
Query.orderDesc("createdAt"),
Query.limit(50),
]
);
return response.documents as unknown as Bookmark[];
}
export async function deleteBookmark(bookmarkId: string) {
const { databases } = createAdminClient();
await databases.deleteDocument(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
bookmarkId
);
revalidatePath("/dashboard");
}Étape 8 : Construire l'interface du tableau de bord
Composant carte de favori
// src/components/BookmarkCard.tsx
"use client";
import { Bookmark, deleteBookmark } from "@/lib/actions/bookmarks";
import { useState } from "react";
export function BookmarkCard({ bookmark }: { bookmark: Bookmark }) {
const [isDeleting, setIsDeleting] = useState(false);
async function handleDelete() {
if (!confirm("Supprimer ce favori ?")) return;
setIsDeleting(true);
await deleteBookmark(bookmark.$id);
}
return (
<div className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition group">
<div className="flex items-start gap-3">
<img src={bookmark.favicon} alt="" className="w-6 h-6 mt-1 rounded" loading="lazy" />
<div className="flex-1 min-w-0">
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-medium truncate block"
>
{bookmark.title}
</a>
{bookmark.description && (
<p className="text-sm text-gray-500 mt-1 line-clamp-2">{bookmark.description}</p>
)}
<div className="flex items-center gap-2 mt-2">
{bookmark.tags.map((tag) => (
<span key={tag} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-700">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
);
}Formulaire d'ajout de favori
// src/components/AddBookmarkForm.tsx
"use client";
import { createBookmark } from "@/lib/actions/bookmarks";
import { useAuth } from "@/lib/auth/context";
import { useState } from "react";
export function AddBookmarkForm() {
const { user } = useAuth();
const [isOpen, setIsOpen] = useState(false);
if (!user) return null;
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
+ Ajouter un favori
</button>
{isOpen && (
<form
action={async (formData) => {
formData.set("userId", user.$id);
await createBookmark(formData);
setIsOpen(false);
}}
className="mt-4 bg-white rounded-lg border border-gray-200 p-6 space-y-4"
>
<div>
<label className="block text-sm font-medium text-gray-700">URL</label>
<input name="url" type="url" required placeholder="https://example.com" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Titre</label>
<input name="title" required placeholder="Titre de la page" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description" rows={2} placeholder="Brève description (optionnel)" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Tags</label>
<input name="tags" placeholder="react, nextjs, tutorial (séparés par des virgules)" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div className="flex items-center gap-2">
<input name="isPublic" type="checkbox" value="true" id="isPublic" />
<label htmlFor="isPublic" className="text-sm text-gray-700">Rendre ce favori public</label>
</div>
<div className="flex gap-3">
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Enregistrer</button>
<button type="button" onClick={() => setIsOpen(false)} className="px-4 py-2 text-gray-600 hover:text-gray-800">Annuler</button>
</div>
</form>
)}
</div>
);
}Étape 9 : Ajouter le stockage de fichiers pour les vignettes
Créez un bucket de stockage dans la Console Appwrite :
- Allez dans Storage et créez un bucket appelé
thumbnails - Définissez la taille maximale des fichiers à 5 Mo
- Autorisez les extensions :
jpg,jpeg,png,webp,gif - Définissez les permissions : Utilisateurs peuvent créer et lire
// src/lib/actions/storage.ts
"use server";
import { createAdminClient } from "@/lib/appwrite/server";
import { THUMBNAILS_BUCKET_ID } from "@/lib/appwrite/config";
import { ID } from "node-appwrite";
export async function uploadThumbnail(formData: FormData) {
const { storage } = createAdminClient();
const file = formData.get("file") as File;
if (!file || file.size === 0) return null;
const response = await storage.createFile(
THUMBNAILS_BUCKET_ID,
ID.unique(),
file
);
return response.$id;
}Étape 10 : Ajouter les abonnements temps réel
Appwrite fournit des capacités temps réel via des abonnements WebSocket. Ajoutons des mises à jour en direct lorsque les favoris changent :
// src/hooks/useRealtimeBookmarks.ts
"use client";
import { useEffect } from "react";
import { client } from "@/lib/appwrite/client";
import { DATABASE_ID, BOOKMARKS_COLLECTION_ID } from "@/lib/appwrite/config";
import { Bookmark } from "@/lib/actions/bookmarks";
export function useRealtimeBookmarks(
userId: string,
onUpdate: (bookmarks: Bookmark[]) => void,
currentBookmarks: Bookmark[]
) {
useEffect(() => {
const channel = `databases.${DATABASE_ID}.collections.${BOOKMARKS_COLLECTION_ID}.documents`;
const unsubscribe = client.subscribe(channel, (response) => {
const { events, payload } = response as any;
if (payload.userId !== userId) return;
let updated = [...currentBookmarks];
if (events.some((e: string) => e.includes(".create"))) {
updated = [payload, ...updated];
} else if (events.some((e: string) => e.includes(".update"))) {
updated = updated.map((b) => (b.$id === payload.$id ? payload : b));
} else if (events.some((e: string) => e.includes(".delete"))) {
updated = updated.filter((b) => b.$id !== payload.$id);
}
onUpdate(updated);
});
return () => unsubscribe();
}, [userId, currentBookmarks, onUpdate]);
}Maintenant, si vous ouvrez deux onglets du navigateur, la création d'un favori dans un onglet apparaîtra instantanément dans l'autre.
Étape 11 : Ajouter un Middleware pour la protection des routes
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("a_session");
const isAuthPage = request.nextUrl.pathname.startsWith("/login");
const isDashboard = request.nextUrl.pathname.startsWith("/dashboard");
if (isDashboard && !session) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (isAuthPage && session) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/login"],
};Étape 12 : Déployer en production
Déploiement sur Vercel
- Poussez votre code sur GitHub
- Importez le dépôt dans Vercel
- Ajoutez les variables d'environnement dans le tableau de bord Vercel
- Mettez à jour le hostname de la plateforme Appwrite de
localhostvers votre domaine de production
Mettre à jour la plateforme Appwrite
Dans la Console Appwrite, allez dans Overview et ajoutez votre hostname de production. Mettez à jour les URL de redirection OAuth pour inclure le domaine de production.
Tester votre implémentation
- Flux d'authentification : Cliquez "Continuer avec Google" et vérifiez que la redirection OAuth se termine
- Créer des favoris : Ajoutez quelques favoris avec différents tags et vérifiez leur apparition
- Synchronisation temps réel : Ouvrez deux onglets, ajoutez un favori dans l'un, et confirmez son apparition dans l'autre
- Filtrage par tags : Cliquez sur les tags pour filtrer les favoris
- Supprimer des favoris : Survolez un favori et cliquez sur l'icône de suppression
Dépannage
Erreur "Missing required attribute"
Assurez-vous que tous les attributs requis dans la collection Appwrite correspondent aux données envoyées. Vérifiez les noms et types d'attributs dans la Console.
La redirection OAuth échoue
- Vérifiez que l'URL de redirection correspond exactement dans votre code et les paramètres du fournisseur OAuth
- Assurez-vous que le hostname de la plateforme Appwrite inclut le bon domaine
- Vérifiez que le fournisseur OAuth est activé dans les paramètres d'authentification Appwrite
Les événements temps réel ne se déclenchent pas
- Confirmez que les permissions de la collection autorisent la lecture
- Vérifiez que la connexion WebSocket est établie (cherchez les connexions
wss://dans les DevTools) - Assurez-vous de vous abonner au bon format de canal
Appwrite comparé aux autres fournisseurs BaaS
| Fonctionnalité | Appwrite | Supabase | Firebase |
|---|---|---|---|
| Open Source | Oui | Oui | Non |
| Auto-hébergement | Docker | Docker | Non |
| Base de données | Documents (MariaDB) | PostgreSQL | Documents (Firestore) |
| Fournisseurs Auth | Plus de 30 | Plus de 20 | Plus de 20 |
| Temps réel | WebSocket | WebSocket | WebSocket |
| Stockage fichiers | Intégré | Intégré | Intégré |
| Fonctions Cloud | Runtimes multiples | Edge Functions | Cloud Functions |
| Niveau gratuit | Généreux | Généreux | Limité |
Appwrite se distingue par son modèle de base de données documentaire, son support étendu de fournisseurs OAuth, et la possibilité d'auto-héberger toute la stack avec une seule commande Docker Compose.
Prochaines étapes
- Ajouter des fonctions cloud — Créez des Appwrite Functions pour générer automatiquement des aperçus de favoris
- Implémenter des collections — Permettez aux utilisateurs d'organiser les favoris en dossiers
- Ajouter une extension navigateur — Construisez une extension Chrome pour sauvegarder des favoris en un clic
- Export et import — Supportez l'export des favoris en JSON et l'import depuis les fichiers de favoris du navigateur
- Partage collaboratif — Permettez aux utilisateurs de partager des collections de favoris avec des équipes
Conclusion
Vous avez construit un gestionnaire de favoris complet en utilisant Appwrite Cloud et Next.js 15. L'application comprend l'authentification OAuth, une base de données documentaire avec organisation par tags, le stockage de fichiers pour les vignettes, et des abonnements temps réel pour les mises à jour en direct. Appwrite fournit une alternative open source auto-hébergeable aux plateformes BaaS propriétaires, vous donnant un contrôle total sur votre backend tout en maintenant la productivité du développeur. La combinaison des bases de données documentaires Appwrite avec les Actions Serveur Next.js crée une architecture propre et typée qui évolue du prototype à la production.
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.

Construire une application temps réel avec Supabase et Next.js 15 : guide complet
Apprenez à construire une application full-stack en temps réel avec Supabase et Next.js 15 App Router. Ce guide couvre l'authentification, la base de données, Row Level Security et les abonnements temps réel.

Construire une application complète avec Firebase et Next.js 15 : Auth, Firestore et temps réel
Apprenez à créer une application full-stack avec Next.js 15 et Firebase. Ce guide couvre l'authentification, Firestore, les mises à jour en temps réel, les Server Actions et le déploiement sur Vercel.