InstantDB est une base de données temps réel et multijoueur destinée aux applications front-end — souvent décrite comme l'alternative moderne à Firebase. Au lieu de connecter séparément une base de données, une couche API, un serveur WebSocket et un cache côté client, InstantDB vous offre une seule base de données côté client qui se synchronise instantanément sur chaque appareil connecté, fonctionne hors ligne et applique des mises à jour optimistes par défaut.
Ce qui distingue InstantDB en 2026, c'est son modèle de graphe relationnel combiné à la synchronisation instantanée. Vous définissez des entités et des liens typés, vous les interrogez avec une petite syntaxe déclarative appelée InstaQL, et vous les modifiez avec des transactions InstaML. Chaque changement se reflète immédiatement à l'écran et se propage à tous les autres clients en temps réel — sans abonnements manuels ni invalidation de cache.
Dans ce tutoriel, vous allez créer un tableau de tâches collaboratif en temps réel — une liste partagée où plusieurs utilisateurs peuvent ajouter, terminer et supprimer des tâches qui se synchronisent instantanément entre les navigateurs. Vous ajouterez aussi l'authentification par code magique, verrez qui est en ligne grâce à la présence, et protégerez vos données avec des permissions.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un compte InstantDB gratuit — inscrivez-vous sur instantdb.com/dash
- Des connaissances de base en React et TypeScript
- Une familiarité avec l'App Router de Next.js (layouts, composants client, la directive
"use client") - Un éditeur de code (VS Code recommandé)
Ce que vous allez créer
Un tableau de tâches collaboratif avec :
- Un schéma typé reliant
tasksà$users - Des lectures en temps réel avec InstaQL
- Création, mise à jour et suppression optimistes avec InstaML
- Authentification par e-mail via code magique
- Présence en direct montrant qui consulte le tableau
- Des règles de permission côté serveur pour que chacun n'accède qu'à ses propres données
Commençons.
Étape 1 : Créer le projet Next.js
Initialisez une nouvelle application Next.js 15 avec l'App Router et TypeScript :
npx create-next-app@latest instant-board --typescript --app --tailwind
cd instant-boardInstallez ensuite le SDK React d'InstantDB et l'outil en ligne de commande :
npm install @instantdb/react
npm install -D instant-cliLe paquet @instantdb/react fournit les hooks, le proxy de transactions et le client d'authentification que vous utiliserez tout au long de ce tutoriel.
Étape 2 : Initialiser InstantDB
Lancez l'outil en ligne de commande pour connecter votre projet local à une application InstantDB. Cela vous authentifie dans le navigateur et crée les fichiers de schéma et de permissions :
npx instant-cli@latest initLorsque l'on vous le demande, créez une nouvelle application (ou choisissez-en une existante). L'outil écrit deux fichiers à la racine de votre projet :
instant.schema.ts— votre modèle de données typéinstant.perms.ts— vos règles de contrôle d'accès
Il affiche également votre App ID. Ajoutez-le à un fichier .env.local afin qu'il ne soit jamais codé en dur :
# .env.local
NEXT_PUBLIC_INSTANT_APP_ID=your-app-id-hereLe préfixe NEXT_PUBLIC_ est requis pour que l'App ID soit disponible dans le navigateur. L'App ID n'est pas un secret — votre sécurité provient des règles de permission, que nous configurons à l'étape 8.
Étape 3 : Définir votre schéma
Ouvrez instant.schema.ts et définissez une entité tasks ainsi qu'un lien vers l'espace de noms intégré $users. InstantDB fournit automatiquement $users lorsque vous utilisez l'authentification.
// instant.schema.ts
import { i } from "@instantdb/react";
const _schema = i.schema({
entities: {
$users: i.entity({
email: i.string().unique().indexed(),
}),
tasks: i.entity({
text: i.string(),
done: i.boolean(),
createdAt: i.date().indexed(),
}),
},
links: {
taskOwner: {
forward: { on: "tasks", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "tasks" },
},
},
});
// Aides TypeScript pour une sûreté de typage de bout en bout
type _AppSchema = typeof _schema;
interface AppSchema extends _AppSchema {}
const schema: AppSchema = _schema;
export type { AppSchema };
export default schema;Quelques points à noter :
i.string(),i.boolean()eti.date()déclarent des attributs typés..indexed()rend un champ rapide à filtrer et à trier — indexez toujours les champs que vous interrogez.- Le lien
taskOwnerdéfinit une relation un-à-plusieurs : chaque tâche a un propriétaire, et chaque utilisateur a plusieurs tâches.
Poussez le schéma vers InstantDB pour que le cloud en ait connaissance :
npx instant-cli@latest push schemaÉtape 4 : Initialiser le client
Créez une instance db unique et partagée. Comme nous transmettons le schéma, chaque requête et transaction sera entièrement vérifiée par le typage.
// lib/db.ts
import { init } from "@instantdb/react";
import schema from "../instant.schema";
export const db = init({
appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!,
schema,
});Pour les applications Next.js nécessitant un rendu côté serveur du contenu authentifié, InstantDB fournit aussi init depuis @instantdb/react/nextjs, qui synchronise la session d'authentification via une route API de première partie. Pour ce tableau rendu côté client, l'import standard depuis @instantdb/react suffit.
Étape 5 : Ajouter l'authentification par code magique
La méthode d'authentification la plus simple d'InstantDB est le code magique : l'utilisateur saisit un e-mail, reçoit un code à six chiffres, puis le colle. Aucun mot de passe, aucune configuration OAuth.
Créez un composant de connexion :
// components/Login.tsx
"use client";
import { useState } from "react";
import { db } from "@/lib/db";
export function Login() {
const [sentEmail, setSentEmail] = useState("");
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const sendCode = (e: React.FormEvent) => {
e.preventDefault();
setSentEmail(email);
db.auth.sendMagicCode({ email }).catch((err) => {
alert("Error: " + err.body?.message);
setSentEmail("");
});
};
const verifyCode = (e: React.FormEvent) => {
e.preventDefault();
db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => {
alert("Error: " + err.body?.message);
setCode("");
});
};
if (!sentEmail) {
return (
<form onSubmit={sendCode} className="flex flex-col gap-3 max-w-sm">
<h2 className="text-xl font-bold">Sign in</h2>
<input
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border px-3 py-2 rounded"
required
/>
<button className="bg-blue-600 text-white px-3 py-2 rounded">
Send code
</button>
</form>
);
}
return (
<form onSubmit={verifyCode} className="flex flex-col gap-3 max-w-sm">
<h2 className="text-xl font-bold">Enter your code</h2>
<p>We emailed a code to {sentEmail}.</p>
<input
type="text"
placeholder="123456"
value={code}
onChange={(e) => setCode(e.target.value)}
className="border px-3 py-2 rounded"
required
/>
<button className="bg-blue-600 text-white px-3 py-2 rounded">
Verify
</button>
</form>
);
}Les deux appels qui font tout le travail sont db.auth.sendMagicCode({ email }) et db.auth.signInWithMagicCode({ email, code }). En cas de succès, InstantDB stocke la session et met à jour chaque composant qui lit l'état d'authentification.
Étape 6 : Protéger l'application avec useAuth
Utilisez le hook db.useAuth() pour décider d'afficher l'écran de connexion ou le tableau. Il renvoie l'état de chargement, l'utilisateur courant et toute erreur.
// app/page.tsx
"use client";
import { db } from "@/lib/db";
import { Login } from "@/components/Login";
import { Board } from "@/components/Board";
export default function Home() {
const { isLoading, user, error } = db.useAuth();
if (isLoading) return <div className="p-8">Loading...</div>;
if (error) return <div className="p-8">Auth error: {error.message}</div>;
return (
<main className="p-8 max-w-2xl mx-auto">
{user ? <Board user={user} /> : <Login />}
</main>
);
}Étape 7 : Lire et écrire en temps réel
Voici le cœur de l'application. Créez le composant Board qui lit les tâches avec InstaQL et les modifie avec InstaML.
// components/Board.tsx
"use client";
import { useState } from "react";
import { id, type User } from "@instantdb/react";
import { db } from "@/lib/db";
export function Board({ user }: { user: User }) {
const [text, setText] = useState("");
// InstaQL : lire les tâches de l'utilisateur courant, triées par date de création
const { isLoading, error, data } = db.useQuery({
tasks: {
$: {
where: { "owner.id": user.id },
order: { createdAt: "desc" },
},
},
});
const addTask = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) return;
const taskId = id();
// InstaML : créer la tâche et la lier à l'utilisateur en une seule transaction
db.transact(
db.tx.tasks[taskId]
.update({ text, done: false, createdAt: Date.now() })
.link({ owner: user.id }),
);
setText("");
};
const toggle = (taskId: string, done: boolean) => {
db.transact(db.tx.tasks[taskId].update({ done: !done }));
};
const remove = (taskId: string) => {
db.transact(db.tx.tasks[taskId].delete());
};
if (isLoading) return <div>Loading tasks...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Task Board</h1>
<button onClick={() => db.auth.signOut()} className="text-sm underline">
Sign out
</button>
</div>
<form onSubmit={addTask} className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What needs doing?"
className="border px-3 py-2 rounded flex-1"
/>
<button className="bg-blue-600 text-white px-4 rounded">Add</button>
</form>
<ul className="flex flex-col gap-2">
{data.tasks.map((task) => (
<li
key={task.id}
className="flex items-center gap-3 border rounded px-3 py-2"
>
<input
type="checkbox"
checked={task.done}
onChange={() => toggle(task.id, task.done)}
/>
<span className={task.done ? "line-through opacity-60" : ""}>
{task.text}
</span>
<button
onClick={() => remove(task.id)}
className="ml-auto text-red-600 text-sm"
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}Trois concepts font fonctionner ce composant :
- InstaQL —
db.useQuery({ tasks: { ... } })est un abonnement en direct. L'opérateur$contient les options de requête commewhereetorder. Lorsqu'un client modifie une tâche correspondante, ce composant se réaffiche automatiquement. - InstaML —
db.transact(db.tx.tasks[taskId].update(...))décrit une mutation. Le proxydb.txsuit la formedb.tx.NAMESPACE[ID].ACTION(DATA), où les actions incluentcreate,update,merge,delete,linketunlink. id()— génère un nouvel UUID pour les nouvelles entités afin de construire l'interface optimiste avant la réponse du serveur.
Comme InstantDB applique les mises à jour optimistes localement, une nouvelle tâche apparaît à l'instant où vous soumettez le formulaire — avant même la fin de l'aller-retour.
Étape 8 : Verrouiller avec les permissions
Par défaut, une application InstantDB est ouverte pour vous permettre de prototyper rapidement. Avant la mise en production, vous devez restreindre l'accès. Ouvrez instant.perms.ts et exigez que les utilisateurs ne lisent et n'écrivent que leurs propres tâches.
// instant.perms.ts
import type { InstantRules } from "@instantdb/react";
const rules = {
tasks: {
allow: {
view: "auth.id != null && auth.id == data.ref('owner.id')",
create: "auth.id != null && auth.id == newData.ref('owner.id')",
update: "auth.id != null && auth.id == data.ref('owner.id')",
delete: "auth.id != null && auth.id == data.ref('owner.id')",
},
},
} satisfies InstantRules;
export default rules;Ces règles utilisent auth.id (l'identifiant de l'utilisateur authentifié) et data.ref('owner.id') (le propriétaire lié de la tâche) pour garantir que personne ne peut lire ou modifier des tâches qu'il ne possède pas. Poussez les règles vers le cloud :
npx instant-cli@latest push permsNe déployez jamais une application avec les permissions ouvertes par défaut. L'App ID est public, donc n'importe qui pourrait lire ou écrire vos données tant que les règles ne sont pas en place. Testez vos règles en vous connectant avec deux utilisateurs différents et en confirmant que chacun ne voit que ses propres tâches.
Étape 9 : Ajouter la présence en direct
La présence montre qui consulte actuellement le tableau en temps réel, sans rien stocker dans la base de données. Créez une salle et publiez le statut de chaque utilisateur.
// components/PresenceBar.tsx
"use client";
import { db } from "@/lib/db";
import type { User } from "@instantdb/react";
const room = db.room("board", "main");
export function PresenceBar({ user }: { user: User }) {
const { peers } = db.rooms.usePresence(room);
db.rooms.useSyncPresence(room, { email: user.email });
const others = Object.values(peers);
return (
<div className="text-sm text-gray-600">
{others.length === 0
? "You're the only one here"
: `${others.length} other ${others.length === 1 ? "person" : "people"} online: ` +
others.map((p) => p.email).join(", ")}
</div>
);
}Ici, db.room("board", "main") définit un canal éphémère partagé. useSyncPresence publie les données de l'utilisateur courant, et usePresence renvoie les peers actuellement dans la salle. Insérez <PresenceBar user={user} /> dans le Board et vous obtenez instantanément un indicateur en direct de « qui est en ligne ». La même primitive de salle alimente les indicateurs de saisie, les curseurs en direct et les réactions.
Tester votre implémentation
- Lancez
npm run devet ouvrez http://localhost:3000. - Connectez-vous avec votre e-mail et le code magique.
- Ajoutez quelques tâches — notez qu'elles apparaissent instantanément grâce aux mises à jour optimistes.
- Ouvrez l'application dans une seconde fenêtre de navigateur (ou un onglet privé) et connectez-vous avec un autre e-mail. Confirmez que chaque compte ne voit que ses propres tâches (vos règles de permission à l'œuvre).
- Ouvrez le même compte dans deux onglets et observez la synchronisation des tâches en temps réel entre les deux.
- Observez la barre de présence se mettre à jour à mesure que vous ouvrez et fermez des onglets.
Dépannage
- « Permission denied » sur chaque requête. Vos règles font probablement référence à un lien inexistant, ou vous avez oublié de les pousser. Lancez
npx instant-cli@latest push permset confirmez que le lientaskOwnerfigure dans votre schéma. - Les tâches n'apparaissent pas après actualisation. Assurez-vous d'avoir poussé le schéma (
push schema) et quecreatedAtest indexé — trier par un champ non indexé échoue. - Erreurs de typage sur
db.useQuery. Vérifiez quelib/db.tstransmet l'argumentschemaàinit. Sans cela, les requêtes ne sont pas typées. - Le code magique n'arrive jamais. Vérifiez les spams et confirmez que l'App ID dans
.env.localcorrespond au tableau de bord. Redémarrez le serveur de développement après avoir modifié les fichiers d'environnement.
Étapes suivantes
- Ajoutez des curseurs en direct en utilisant la même primitive
db.roomavecuseTopicEffectpour les événements éphémères. - Partagez des tableaux entre utilisateurs en ajoutant une entité
boardsavec un lien plusieurs-à-plusieurs vers$users. - Passez côté serveur avec le SDK
@instantdb/adminpour initialiser des données ou exécuter des mutations de confiance depuis des routes API. - Explorez les tutoriels connexes sur ce site : applications temps réel Convex, synchronisation ElectricSQL et applications local-first avec Yjs.
Conclusion
En moins de 30 minutes, vous avez construit un tableau de tâches temps réel, multijoueur et capable de fonctionner hors ligne avec InstantDB et Next.js 15 — sans routes API, sans plomberie WebSocket et sans gestion de cache. Vous avez défini un schéma relationnel typé, lu des données avec InstaQL, les avez modifiées avec InstaML, authentifié les utilisateurs avec des codes magiques, tout sécurisé avec des règles de permission et ajouté la présence en direct.
La promesse d'InstantDB est que le temps réel et le hors ligne ne sont pas des fonctionnalités à ajouter après coup — ce sont les valeurs par défaut. Ce changement permet aux développeurs front-end de livrer des logiciels collaboratifs avec le même effort qu'il fallait autrefois pour construire une simple application CRUD statique.