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.


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, 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.

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.
┌─────────────────────────────────────────────────┐
│                  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é.

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.

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, 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.

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.

┌─────────────────────────────────────────────────┐
│              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.

  • 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.

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)
 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, 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 ?

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.

┌───────────────────────────────────────────────────────────────┐
│                       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 — 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.

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)
 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});

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

                    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.

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!