SQLite, CubDB et Turso : quand un fichier suffit pour stocker tes données
SQLite, CubDB et Turso : trois approches de la base de données embarquée, du classique au distribué.

Il m’arrive de lancer un Postgres pour un projet perso qui va jamais dépasser trois users. Une base PostgreSQL, un Docker, un dump, des migrations… pour stocker quarante lignes de config. C’est débile. Et je suis pas le seul.
Il existe une autre voie. Une voie où ta base de données vit dans un fichier unique, sans serveur, sans Docker, sans infra. Juste un fichier que tu copies, que tu backup, et qui marche partout. Trois outils incarnent cette philosophie : SQLite, CubDB, et Turso.
On va voir ce que c’est, pourquoi c’est utile, et quand utiliser quoi.
Lexique
C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.
- Embedded database: base de données qui tourne dans le même processus que ton application, pas sur un serveur séparé.
- B-tree: structure de données en arbre utilisée pour indexer et retrouver rapidement des données sur disque.
- WAL (Write-Ahead Logging): technique d’écriture où les changements sont d’abord loggués dans un fichier séparé avant d’être appliqués à la base principale.
- MVCC (Multi-Version Concurrency Control): mécanisme qui permet à plusieurs lectures d’avoir lieu en même temps qu’une écriture, chacune voyant un “snapshot” cohérent.
- Compaction: opération de nettoyage qui réorganise le fichier de données pour récupérer l’espace disque inutilisé.
- OTP (Open Telecom Platform): le framework de supervision et de fault-tolerance d’Erlang/Elixir.
- NIF (Native Implemented Function): fonction écrite en C/Rust appelée directement depuis le BEAM — performant mais risqué (crash du VM si ça merde).
- libSQL: fork open-source de SQLite, maintenu par l’équipe Turso.
- io_uring: interface d’E/Linux pour de l’asynchrone pur, ultra-performante.
SQLite — Le standard silencieux
Ce que c’est
SQLite, c’est une librairie C qui implémente un moteur SQL complet. Pas un serveur. Pas un daemon. Un fichier. Tu l’include dans ton projet, tu linkes la lib, et tu as une base de données relationnelle avec joins, triggers, CTEs, window functions — tout le bazar.
C’est public domain. Ça existe depuis 2004. Le format de fichier est garanti compatible à vie. C’est la base de données la plus déployée au monde : chaque smartphone, chaque navigateur, chaque OS, des milliards de devices.
Et c’est 700KB de code C. Putain.
Pourquoi c’est bien
SQLite résout un problème fondamental : stocker des données structurées sans infra.
- Zéro config. Pas de serveur à installer, configurer, maintenir. Tu copies le binaire et tu lances.
- Un fichier, c’est tout. Ta base entière vit dans un fichier que tu copies, emails, backup, versionnes.
- Cross-platform. Le fichier marche entre 32/64-bit, big/little-endian, Windows, Linux, macOS. Sans conversion.
- Transactions ACID. Les commits sont atomiques — crash ou blackout, tes données sont safe.
- SQL complet. Pas un sous-ensemble de merde. Du vrai SQL, avec des index, des vues, des CTEs.
- Zero-copy reads. SQLite ne lit que les pages dont il a besoin. Pas besoin de charger tout en mémoire.
- WAL mode. Les lectures concurrentes ne bloquent plus les écritures. Pour des workloads read-heavy, c’est 100x plus rapide que le mode rollback par défaut.
- Battle-tested. 100% de couverture de branches de test. Le Library of Congress le recommande pour la préservation numérique à long terme.
Architecture
┌─────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────┤
│ SQLite Library (C API) │
├─────────────────────────────────────────────────┤
│ SQL Parser ──► VDBE (Machine Virtuelle) │
├─────────────────────────────────────────────────┤
│ B-Tree Engine / Page Cache │
├─────────────────────────────────────────────────┤
│ Interface OS (VFS) │
├─────────────────────────────────────────────────┤
│ Fichier Unique (.sqlite / .db) │
└─────────────────────────────────────────────────┘
Le pipeline : SQL → Parser → Compilation en bytecode → Exécution VM → Traversal B-tree → Page cache → I/O disque via le VFS. C’est simple, c’est efficace, c’est testé.
WAL : le game-changer
En mode rollback (défaut), SQLite sérialise toutes les écritures via un verrou unique. Les lectures sont bloquées pendant une écriture. C’est lent.
Le mode WAL change la donne :
| Mode | Lecture pendant écriture | Concurrence écriture | Perf |
|---|---|---|---|
| Rollback (défaut) | Bloquée | Un seul writer, sérialisé | Baseline |
| WAL | Autorisée | Un seul writer, lectures concurrentes | ~100x pour read-heavy |
C’est un seul pragma à l’ouverture de la base. C’est le changement le plus impactant pour une utilisation SQLite en production.
Quand l’utiliser
Parfait pour :
- Apps desktop et mobile
- Systèmes embarqués (IoT, Nerves, devices)
- Apps web single-server (traffic modéré, < 1000 users concurrents)
- Prototypage et dev (remplace Postgres en CI pour la vitesse)
- Apps local-first / offline-first
- Analyse de données
- Format de fichier applicatif (remplace JSON/XML/binary configs)
Pas adapté pour :
- Workloads write intensifs (centaines de writes/sec)
- Accès multi-serveurs au même fichier (NFS locking, c’est de la merde)
- Bases approchant le téraoctet
- Extensions spécifiques à Postgres (géospatial, two-phase commit)
CubDB — Le Map Elixir qui survit aux crashes
Ce que c’est
CubDB, c’est une base de données clé-valeur embarquée, écrite entièrement en Elixir. Pas de NIF, pas de code natif. Du pur BEAM.
Pense à un Map Elixir qui persiste sur disque. Schema-less, rapide, supervisé comme n’importe quel processus OTP. Tu le mets sous un supervisor, il survit aux crashes, et il te coûte rien tant que tu l’utilises pas.
La diff avec SQLite
| Dimension | SQLite | CubDB |
|---|---|---|
| Langage | Librairie C | Elixir pur (pas de NIFs) |
| Modèle données | Relationnel (tables, SQL) | Clé-valeur (n’importe quel terme Elixir) |
| Langage de requête | SQL | Opérations clé/valeur, sélections de ranges |
| Schema | Obligatoire (CREATE TABLE) | Schema-less |
| Supervision | Processus externe ou lib embarquée | GenServer OTP, totalement supervisé |
| Cross-platform | Tout OS/arch via C | Partout où tourne le BEAM |
| Use case principal | Données structurées généralistes | État local, caches, devices embarqués |
CubDB stocke des termes Elixir arbitraires comme clés et valeurs. Atomes, tuples, structs, maps imbriquées — tout passe. C’est idéal pour des données qui se naturellement représentent en maps et listes plutôt qu’en lignes et colonnes.
C’est pas une base SQL. Si tu as besoin de joins, de CTEs, de window functions, utilise SQLite ou Postgres.
Architecture
┌─────────────────────────────────────────────────┐
│ Application Elixir │
├─────────────────────────────────────────────────┤
│ CubDB GenServer (Process OTP) │
│ ┌───────────────────────────────────────────┐ │
│ │ MVCC + Snapshots Immutables │ │
│ │ File d'attente d'écritures │ │
│ └───────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ B-Tree Immutable Append-Only │
│ (inspiré du format CouchDB) │
├─────────────────────────────────────────────────┤
│ Fichier Unique (.cub) sur Disque │
└─────────────────────────────────────────────────┘
La structure clé : un B-tree immutable append-only. Les entrées ne sont jamais modifiées en place. Chaque écriture ajoute de nouveaux nœuds à la fin du fichier. Les lectures tournent sur des snapshots immutables à coût zéro — pas de copie, pas de verrou.
Compaction : parce que les écritures n’ajoutent que, le fichier grossit. CubDB lance une compaction en arrière-plan pour récupérer l’espace : il crée un nouveau fichier avec uniquement les entrées accessibles, puis bascule dessus atomiquement. Non-bloquant, automatique ou manuel.
Features clés
- Transactions ACID avec MVCC — les lectures concurrentes ne bloquent jamais les écritures.
- Snapshots à coût zéro — vues à un instant T sans overhead de copie.
- Crash-safe — le design append-only signifie qu’une écriture partielle corrompt pas les données.
- Supervisé — démarre sous un OTP supervisor comme n’importe quel GenServer.
- Termes Elixir — clés et valeurs peuvent être des atoms, tuples, structs, maps imbriquées.
- Range selects — itération triée efficace sur des plages de clés.
- Pas de NIFs — pur BEAM, pas de code natif pour crasher le VM.
Quand l’utiliser
Parfait pour :
- Apps Elixir single-instance needing stockage clé-valeur persistant
- Devices embarqués / edge avec Nerves
- État applicatif qui survit aux restarts de processus
- Caches locaux et session stores
- Partout où tu utiliserais une ETS table mais où tu as besoin de persistance disque
Pas adapté pour :
- Requêtes relationnelles complexes (utilise Postgres/SQLite)
- Accès distribué multi-node (CubDB est single-instance)
- Full-text search ou indexage complexe (pas de moteur SQL)
Usage
1# Démarrer CubDB sous supervision
2{:ok, db} = CubDB.start_link(data_dir: "my_data")
3
4# Opérations de base
5CubDB.put(db, :user_1, %{name: "Alice", age: 30})
6CubDB.get(db, :user_1)
7# => %{name: "Alice", age: 30}
8
9CubDB.delete(db, :user_1)
10
11# Range select
12CubDB.select(db, min_key: :user_1, max_key: :user_99)
13
14# Lecture multi-clés atomique
15CubDB.with_snapshot(db, fn snap ->
16 x = CubDB.Snapshot.get(snap, :counter)
17 y = CubDB.Snapshot.get(snap, :config)
18 {x, y}
19end)Turso — SQLite qui a grandi
Ce que c’est
Turso, c’est un peu la suite logique de SQLite. C’est un projet porté par la même équipe, construit sur deux piliers :
- libSQL — un fork open-source de SQLite (en C) qui ajoute des replicas embarquées, l’accès distant, et la recherche vectorielle, tout en restant compatible SQLite.
- Turso Database — une réécriture complète de SQLite en Rust, avec écritures concurrentes (MVCC), I/O async, et architecture multi-database à l’échelle.
Turso Cloud héberge ces bases, en traitant chacune comme un fichier léger — pas un processus long-running. Ça permet des millions de petites bases isolées, sans penalty de cold start.
Mais du coup, il fait quoi de différent par rapport à SQLite putain ?
La diff avec SQLite
| Dimension | SQLite | Turso |
|---|---|---|
| Implémentation | Librairie C | Rust (Turso DB) ou fork C (libSQL) |
| Écritures concurrentes | Un seul writer (sérialisé) | Plusieurs writers via MVCC |
| Réplication | Pas intégrée | Réplicas embarquées, sync global |
| I/O Async | VFS synchrone | io_uring, async-first |
| Échelle multi-DB | Manuel (un fichier = une DB) | Millions de bases comme fichiers |
| Recherche vectorielle | Via extensions | Native intégrée |
| Cloud | Pas de first-party | Turso Cloud (managed) |
| Offline/sync | Non | Réplicas embarquées avec push/pull |
| License | Public Domain | MIT |
Le changement fondamental : SQLite traite une base comme un fichier possédé par un seul processus. Turso traite une base comme un fichier qui peut être répliqué globalement, synchronisé vers des devices, et géré par une plateforme cloud — tout en restant un fichier.
Architecture
┌───────────────────────────────────────────────────────────────┐
│ Turso Cloud │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Primary │ │ Replica │ │ S3 / S3 Express │ │
│ │ (Région) │──│ (Région) │ │ (Durabilité) │ │
│ └──────┬───────┘ └──────┬──────┘ └─────────────────────┘ │
│ │ │ │
│ │ Protocole de Réplication │
│ │ (WAL frames + rolling checksums) │
│ │ │ │
├─────────┼─────────────────┼───────────────────────────────────┤
│ │ │ │
│ ┌──────▼─────────────────▼───────┐ │
│ │ Réplicas Edge / Embarquées │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ DB Local │ │ DB Local │ │ ← lectures du fichier │
│ │ │ (fichier)│ │ (fichier)│ │ ← écritures vers primary │
│ │ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Les concepts clés :
- Database-as-file, pas processus. Une base Turso idle ne coûte que le stockage — pas de processus qui tourne.
- Réplicas embarquées. Une copie locale de la base vit dans ton app. Les lectures sont sub-microseconde (fichier local). Les écritures vont au primary cloud, puis se sync back.
- Réplication via WAL. Les changements coulent comme des WAL frames avec des rolling checksums (chaînage cryptographique pour détecter la falsification).
- Multi-tenant par design. Chaque user, tenant, ou agent a une base de donnée isolée. Pas de state partagé.
libSQL vs Turso Database
- libSQL — fork mature de SQLite, production-ready aujourd’hui. Backward compatible, API identique. C’est le choix sûr si tu as besoin de something battle-tested.
- Turso Database — réécriture Rust en beta. Ajoute écritures concurrentes (MVCC), async I/O (io_uring), et recherche vectorielle native. C’est l’avenir mais c’est encore en cours.
Quand l’utiliser
Parfait pour :
- SaaS multi-tenant (isolation par base de données)
- Edge computing (réplicas proches des users)
- Mémoire d’agents IA (millions de petites bases isolées)
- Apps local-first qui ont besoin de sync cloud
- Apps qui ont besoin de sémantique SQLite mais avec écritures concurrentes
- Apps mobiles avec capability offline
Pas adapté pour :
- Besoins simple single-file (utilise SQLite directement)
- Projets qui ont besoin de maturité battle-tested aujourd’hui (utilise libSQL ou SQLite vanilla)
- Très grosses bases single (Turso excelle sur beaucoup de petites bases)
Usage
1import { createClient } from "@libsql/client";
2
3// Local-only (comme SQLite)
4const client = createClient({ url: "file:local.db" });
5
6// Réplica embarquée (lectures locales, écritures au cloud)
7const client = createClient({
8 url: "file:replica.db",
9 syncUrl: "libsql://my-db.turso.io",
10 authToken: "...",
11});
12
13// Sync périodique
14await client.sync({ full: false });
15
16// SQL standard — identique à SQLite
17await client.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
18await client.execute({
19 sql: "INSERT INTO users (name) VALUES (?)",
20 args: ["Alice"],
21});Matrice de comparaison
| Feature | SQLite | CubDB | Turso |
|---|---|---|---|
| Langage | C | Elixir | Rust |
| License | Public Domain | Apache 2.0 | MIT |
| Modèle données | Relationnel (SQL) | Clé-valeur | Relationnel (SQL) |
| Langage requête | SQL | Opérations clé/valeur | SQL (SQLite-compatible) |
| Schema | Obligatoire | Schema-less | Obligatoire |
| Écritures concurrentes | Un seul writer | Un seul writer (GenServer) | Plusieurs writers (MVCC) |
| Réplication | Pas intégrée | Non | Réplicas embarquées, global |
| I/O Async | Non | BEAM scheduler | io_uring |
| Supervision | N/A | GenServer OTP | N/A |
| Cloud | Non | Non | Turso Cloud |
| Offline sync | Non | Non | Oui |
| Recherche vectorielle | Extensions | Non | Native |
| Taille max pratique | ~100 GB (single file) | Illimité (fichier grossit) | Beaucoup de petites DBs |
Arbre de décision
Besoin de requêtes relationnelles ?
│
┌─────────┴─────────┐
│ Oui │ Non
▼ ▼
Besoin d'écritures Besoin de supervision
concurrentes ou OTP et d'Elixir pur ?
de réplication ? │
│ ┌──────┴──────┐
┌─────┴─────┐ │ Oui │ Non
│ Oui │ Non ▼ ▼
▼ ▼ CubDB Utilise SQLite
Turso SQLite
La règle d’or :
- Commence par SQLite. C’est le choix qui couvre 90% des besoins de base de données embarquée.
- Utilise CubDB quand tu es en Elixir et que tu veux du stockage clé-valeur idiomatique, supervisé, sans NIFs.
- Utilise Turso quand tu as besoin de SQLite distribué, d’isolation multi-tenant, de réplicas edge, ou d’écritures concurrentes.
Mot de la fin
SQLite est le standard silencieux. Il est partout, il marche, il est testé, il est gratuit. La prochaine fois que tu lances un Docker pour un Postgres qui va stocker trois tables, demande-toi si un fichier suffit.
CubDB, c’est l’option Elixir-native quand tu veux du persistant sans te prendre la tête avec du code C. Et Turso, c’est SQLite qui a grandi — qui sort du fichier unique pour devenir distribué, tout en restant un fichier.
Trois outils. Trois philosophies. Un seul principe : un fichier, c’est suffisant.
See ya space-cowboy!