PGlite : exécuter PostgreSQL dans le navigateur avec WebAssembly

Ce que vous allez apprendre
Dans ce tutoriel, vous découvrirez comment utiliser PGlite, un projet open source développé par ElectricSQL qui compile PostgreSQL en WebAssembly (WASM). À la fin de ce guide, vous serez capable de :
- Exécuter une base de données PostgreSQL complète entièrement dans le navigateur — sans serveur
- Persister les données entre les rechargements de page grâce à IndexedDB
- Utiliser des requêtes réactives en temps réel qui mettent automatiquement à jour votre interface
- Construire une application de tâches local-first avec React et TypeScript
- Comprendre quand et pourquoi PGlite est le bon choix pour votre projet
Prérequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20+ installé sur votre machine
- Le gestionnaire de paquets npm ou pnpm
- Des connaissances de base en React et TypeScript
- Une familiarité avec SQL (SELECT, INSERT, UPDATE, DELETE)
- Un navigateur moderne (Chrome, Firefox ou Edge)
Pourquoi PGlite ?
Les applications web traditionnelles nécessitent un serveur de base de données en backend. PGlite change cette équation en intégrant une instance PostgreSQL complète directement dans votre environnement JavaScript. Voici pourquoi cela change la donne :
- Zéro infrastructure : pas de serveur de base de données à provisionner, configurer ou maintenir
- Démarrage instantané : la base de données se lance en quelques millisecondes dans le navigateur
- Support SQL complet : contrairement à IndexedDB ou localStorage, vous bénéficiez du vrai SQL avec jointures, index, opérateurs JSON et extensions
- Hors ligne par défaut : les données résident dans le navigateur — votre application fonctionne sans Internet
- Requêtes réactives : le système de requêtes en direct de PGlite pousse les mises à jour vers votre interface automatiquement
- Léger : le bundle WASM fait environ 3 Mo compressé
PGlite est idéal pour le prototypage, les applications local-first, les outils embarqués, les extensions de navigateur et tout scénario nécessitant la puissance SQL sans la complexité serveur.
Étape 1 : créer un nouveau projet React
Commencez par créer un nouveau projet React avec Vite et TypeScript :
npm create vite@latest pglite-todo -- --template react-ts
cd pglite-todoInstallez les dépendances principales :
npm install @electric-sql/pglite
npm install @electric-sql/pglite-reactLe package @electric-sql/pglite contient le build PostgreSQL WASM, et @electric-sql/pglite-react fournit des hooks React pour les requêtes en direct et la gestion de la base de données.
Étape 2 : initialiser la base de données PGlite
Créez un nouveau fichier src/db.ts pour configurer votre instance de base de données :
// src/db.ts
import { PGlite } from "@electric-sql/pglite";
// Utiliser IndexedDB pour la persistance entre les rechargements
const db = new PGlite("idb://todo-app");
export default db;Le préfixe idb:// indique à PGlite de stocker les données dans IndexedDB. Sans ce préfixe, les données existeraient uniquement en mémoire et disparaîtraient au rechargement. Les autres options de stockage incluent :
memory://— en mémoire uniquement (idéal pour les tests)idb://nom-base— persisté dans IndexedDB- Un chemin de fichier dans Node.js — persisté sur le système de fichiers
Étape 3 : créer le schéma de la base de données
Créez un fichier de migration src/migrate.ts qui configure la table des tâches :
// src/migrate.ts
import db from "./db";
export async function runMigrations() {
await db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
`);
}Il s'agit de DDL PostgreSQL standard — PGlite supporte la même syntaxe SQL que vous utiliseriez avec un serveur Postgres classique. La clause IF NOT EXISTS garantit que la migration est idempotente et peut être exécutée en toute sécurité à chaque démarrage de l'application.
Étape 4 : construire le Provider PGlite
Enveloppez votre application avec un provider qui initialise la base de données avant de rendre les composants enfants. Créez src/PGliteProvider.tsx :
// src/PGliteProvider.tsx
import { PGliteProvider } from "@electric-sql/pglite-react";
import { useEffect, useState, type ReactNode } from "react";
import db from "./db";
import { runMigrations } from "./migrate";
export function DatabaseProvider({ children }: { children: ReactNode }) {
const [ready, setReady] = useState(false);
useEffect(() => {
runMigrations().then(() => setReady(true));
}, []);
if (!ready) {
return <div className="loading">Initialisation de la base de données...</div>;
}
return <PGliteProvider db={db}>{children}</PGliteProvider>;
}Étape 5 : implémenter la liste de tâches avec les requêtes en direct
Créez maintenant le composant principal dans src/TodoApp.tsx. C'est ici que PGlite brille vraiment — le hook useLiveQuery re-rend automatiquement votre composant chaque fois que les données changent :
// src/TodoApp.tsx
import { useLiveQuery, usePGlite } from "@electric-sql/pglite-react";
import { useState } from "react";
interface Todo {
id: number;
title: string;
completed: boolean;
created_at: string;
}
export function TodoApp() {
const db = usePGlite();
const [newTitle, setNewTitle] = useState("");
// Requête en direct — se met à jour automatiquement
const todos = useLiveQuery<Todo>(
"SELECT * FROM todos ORDER BY created_at DESC"
);
const addTodo = async () => {
if (!newTitle.trim()) return;
await db.exec("INSERT INTO todos (title) VALUES ($1)", [newTitle.trim()]);
setNewTitle("");
};
const toggleTodo = async (id: number, completed: boolean) => {
await db.exec("UPDATE todos SET completed = $1 WHERE id = $2", [
!completed,
id,
]);
};
const deleteTodo = async (id: number) => {
await db.exec("DELETE FROM todos WHERE id = $1", [id]);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") addTodo();
};
return (
<div className="todo-app">
<h1>Application de tâches PGlite</h1>
<p className="subtitle">
Propulsée par PostgreSQL fonctionnant dans votre navigateur via WebAssembly
</p>
<div className="input-row">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Que faut-il faire ?"
/>
<button onClick={addTodo}>Ajouter</button>
</div>
<ul className="todo-list">
{todos?.rows.map((todo) => (
<li key={todo.id} className={todo.completed ? "completed" : ""}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id, todo.completed)}
/>
<span>{todo.title}</span>
</label>
<button className="delete" onClick={() => deleteTodo(todo.id)}>
Supprimer
</button>
</li>
))}
</ul>
{todos?.rows.length === 0 && (
<p className="empty">Aucune tâche pour le moment. Ajoutez-en une ci-dessus !</p>
)}
</div>
);
}Remarquez que vous ne rafraîchissez jamais manuellement les résultats des requêtes. Lorsque vous appelez db.exec pour insérer, mettre à jour ou supprimer une ligne, le hook useLiveQuery détecte le changement et re-rend le composant automatiquement. Cela fonctionne de manière similaire aux abonnements en temps réel de Supabase ou Firebase, mais tout se passe localement sans aucune latence réseau.
Étape 6 : assembler l'application
Mettez à jour src/App.tsx pour utiliser le provider de base de données et le composant de tâches :
// src/App.tsx
import { DatabaseProvider } from "./PGliteProvider";
import { TodoApp } from "./TodoApp";
import "./App.css";
function App() {
return (
<DatabaseProvider>
<TodoApp />
</DatabaseProvider>
);
}
export default App;Étape 7 : ajouter le style
Remplacez le contenu de src/App.css par un style soigné pour l'application de tâches :
/* src/App.css */
:root {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background: #f0f0f5;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 1.2rem;
color: #666;
}
.todo-app {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
h1 {
margin: 0 0 0.25rem;
font-size: 1.8rem;
}
.subtitle {
margin: 0 0 1.5rem;
color: #888;
font-size: 0.9rem;
}
.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.input-row input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.input-row input:focus {
outline: none;
border-color: #6c63ff;
}
.input-row button {
padding: 0.75rem 1.5rem;
background: #6c63ff;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.input-row button:hover {
background: #5a52d5;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
}
.todo-list li label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
flex: 1;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #aaa;
}
.delete {
background: none;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.delete:hover {
background: #ffeaea;
}
.empty {
text-align: center;
color: #999;
padding: 2rem 0;
}Étape 8 : lancer et tester l'application
Démarrez le serveur de développement :
npm run devOuvrez votre navigateur à l'adresse http://localhost:5173. Vous devriez voir l'application de tâches. Testez les fonctionnalités suivantes :
- Ajoutez quelques tâches — elles apparaissent instantanément dans la liste
- Cochez et décochez des tâches — l'interface se met à jour en temps réel
- Rechargez la page — vos tâches persistent grâce au stockage IndexedDB
- Ouvrez les DevTools puis Application et IndexedDB — vous pouvez voir les fichiers de la base PGlite
Étape 9 : requêtes et fonctionnalités avancées
L'un des plus grands atouts de PGlite est sa compatibilité totale avec PostgreSQL. Ajoutons quelques fonctionnalités avancées pour démontrer cette puissance.
Recherche en texte intégral
Ajoutez une barre de recherche utilisant la recherche en texte intégral native de PostgreSQL avec tsvector :
// Ajoutez une fonction de recherche à votre TodoApp
const searchTodos = async (query: string) => {
if (!query.trim()) return;
const result = await db.query<Todo>(
`SELECT * FROM todos
WHERE to_tsvector('english', title) @@ plainto_tsquery('english', $1)
ORDER BY created_at DESC`,
[query]
);
return result.rows;
};Statistiques agrégées
Interrogez les statistiques de vos tâches avec les agrégations SQL standard :
const stats = useLiveQuery<{
total: number;
completed: number;
pending: number;
}>(`
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE completed = true) as completed,
COUNT(*) FILTER (WHERE completed = false) as pending
FROM todos
`);Opérations JSON
PGlite supporte les opérateurs JSONB de PostgreSQL, permettant le stockage de documents complexes :
// Stocker des métadonnées en JSONB
await db.exec(`
ALTER TABLE todos ADD COLUMN IF NOT EXISTS
metadata JSONB DEFAULT '{}'::jsonb
`);
// Requêter avec les opérateurs JSON
await db.exec(
`UPDATE todos SET metadata = metadata || $1::jsonb WHERE id = $2`,
[JSON.stringify({ priority: "high", tags: ["work"] }), todoId]
);Étape 10 : utiliser PGlite dans Node.js
PGlite ne se limite pas au navigateur. Vous pouvez utiliser la même API dans Node.js pour des outils CLI, des scripts, des tests et des fonctions serverless :
// node-example.ts
import { PGlite } from "@electric-sql/pglite";
async function main() {
// Dans Node.js, utilisez un chemin de fichier pour la persistance
const db = new PGlite("./my-local-db");
await db.exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
await db.exec(
"INSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT DO NOTHING",
["Alice", "alice@example.com"]
);
const result = await db.query("SELECT * FROM users");
console.log(result.rows);
await db.close();
}
main();Exécutez-le avec :
npx tsx node-example.tsPas de Docker, pas de docker-compose, pas d'installation de Postgres — juste un seul package npm.
Étape 11 : tester avec PGlite
PGlite est excellent pour les tests. Au lieu de simuler votre base de données ou de lancer des conteneurs Docker dans votre CI, utilisez une instance PGlite en mémoire :
// __tests__/todo.test.ts
import { PGlite } from "@electric-sql/pglite";
import { describe, it, expect, beforeEach } from "vitest";
describe("Opérations sur les tâches", () => {
let db: PGlite;
beforeEach(async () => {
// Base de données en mémoire fraîche pour chaque test
db = new PGlite();
await db.exec(`
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false
)
`);
});
it("devrait insérer une tâche", async () => {
await db.exec("INSERT INTO todos (title) VALUES ($1)", ["Acheter du lait"]);
const result = await db.query("SELECT * FROM todos");
expect(result.rows).toHaveLength(1);
expect(result.rows[0].title).toBe("Acheter du lait");
});
it("devrait basculer la complétion", async () => {
await db.exec("INSERT INTO todos (title) VALUES ($1)", ["Exercice"]);
await db.exec("UPDATE todos SET completed = true WHERE id = 1");
const result = await db.query("SELECT completed FROM todos WHERE id = 1");
expect(result.rows[0].completed).toBe(true);
});
});Cette approche vous donne un véritable comportement PostgreSQL dans les tests — pas de mocks, pas de Docker, et les tests s'exécutent en quelques millisecondes.
Résolution de problèmes
Échec du chargement WASM
Si le binaire WASM ne se charge pas, vérifiez que votre bundler est configuré pour gérer les fichiers .wasm. Avec Vite, cela fonctionne directement. Pour Webpack, vous pourriez avoir besoin de :
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};Les données ne persistent pas
Assurez-vous d'utiliser le préfixe idb:// dans le constructeur. Sans lui, PGlite utilise le stockage en mémoire par défaut :
// Persistant
const db = new PGlite("idb://my-app");
// En mémoire uniquement (données perdues au rechargement)
const db = new PGlite();Taille du bundle importante
Le binaire WASM de PGlite fait environ 3 Mo compressé. Pour améliorer le temps de chargement initial :
- Utilisez les imports dynamiques pour charger PGlite de manière différée
- Affichez un indicateur de chargement pendant l'initialisation du WASM
- Envisagez un Service Worker pour mettre en cache le binaire WASM
Erreurs SharedArrayBuffer
Certaines fonctionnalités de PGlite nécessitent SharedArrayBuffer, qui requiert des en-têtes CORS spécifiques :
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Dans Vite, ajoutez ceci à votre configuration :
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
});Quand utiliser PGlite
PGlite est un excellent choix pour :
- Le prototypage : passez directement à la construction sans configurer de base de données
- Les applications local-first : listes de tâches, prise de notes, outils personnels
- Les extensions de navigateur : base de données embarquée avec toute la puissance SQL
- Les tests : remplacez les bases de données Docker par des instances instantanées en mémoire
- Les fonctions edge : exécutez Postgres dans des environnements serverless sans connexions externes
- Les applications hors ligne : PWA nécessitant un stockage de données structuré
Envisagez un serveur de base de données traditionnel quand vous avez besoin de :
- Accès multi-utilisateurs à des données partagées
- Des bases de données de plus de quelques centaines de Mo
- Validation des données et contrôle d'accès côté serveur
- Réplication et haute disponibilité
Prochaines étapes
Maintenant que PGlite fonctionne dans votre navigateur, voici quelques pistes pour approfondir :
- Ajoutez la synchronisation ElectricSQL : combinez PGlite avec ElectricSQL pour synchroniser votre base locale avec une instance Postgres côté serveur
- Construisez une PWA : ajoutez un Service Worker pour rendre votre application entièrement utilisable hors ligne
- Utilisez les extensions PGlite : PGlite supporte des extensions comme
pgvectorpour la recherche par similarité vectorielle directement dans le navigateur - Explorez le support multi-onglets : utilisez la coordination multi-onglets de PGlite pour partager une base de données entre les onglets du navigateur
Conclusion
PGlite représente un changement de paradigme dans notre conception des bases de données pour les applications web. En compilant PostgreSQL en WebAssembly, il apporte toute la puissance du SQL — jointures, index, recherche en texte intégral, JSONB et bien plus — directement dans le navigateur sans aucune infrastructure serveur.
Dans ce tutoriel, vous avez construit une application de tâches complète qui persiste les données localement, réagit automatiquement aux changements et fonctionne entièrement côté client. Vous avez également exploré des fonctionnalités avancées comme la recherche en texte intégral, les opérations JSON et les stratégies de test.
Le mouvement local-first prend de l'ampleur, et PGlite est à son avant-garde. Que vous construisiez un prototype, une extension de navigateur ou une application hors ligne, PGlite vous offre la puissance de base de données dont vous avez besoin sans la charge opérationnelle.
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 applications collaboratives Local-First avec Yjs et React
Apprenez à construire des applications collaboratives en temps réel fonctionnant hors ligne avec les CRDTs Yjs et React. Ce tutoriel couvre la synchronisation sans conflits, l'architecture offline-first et la création d'un éditeur de documents partagé.

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.

Capacitor + React — Créer des applications mobiles multiplateformes depuis votre app web (2026)
Transformez votre application web React en app native iOS et Android avec Capacitor. Ce tutoriel pratique couvre la configuration, les plugins natifs, la caméra, le stockage local, le déploiement sur les stores et les bonnes pratiques de production.