Introduction
React Native ne fournit pas la sauvegarde pour vous. Une sauvegarde/restauration fiable est une décision d’architecture d’application qui dépend d’où vous stockez les données, de la façon dont vous les chiffrez, de ce que chaque plateforme prend en charge et de votre modèle de synchronisation. En production, « sauvegarde React Native » fait la différence entre des utilisateurs qui conservent leur historique et ceux qui repartent de zéro après la perte d’un appareil.
Quatre concepts à garder nets :
- Sauvegarde locale : un instantané ponctuel créé sur l’appareil (souvent un fichier ou une archive) que l’on peut exporter ou restaurer même hors ligne.
- Synchronisation cloud : réplication continue vers un backend. Ce n’est pas un substitut à une sauvegarde ponctuelle ; les suppressions et les bogues se propagent.
- Reprise après sinistre : un processus éprouvé pour remettre un utilisateur en état de marche après perte, vol ou corruption.
- Migration d’appareil : transférer un utilisateur vers un nouveau téléphone sans fuite d’identifiants.
Différentes technologies de stockage exigent des approches différentes. N’insinuez jamais que copier AsyncStorage suffit à lui seul pour une stratégie de sauvegarde complète.
Où React Native stocke les données
Couches de stockage courantes en React Native et ce qu’elles contiennent généralement :
- AsyncStorage : petites préférences clé‑valeur et métadonnées. Non chiffré par défaut. Non transactionnel sur des écritures multi‑clés.
- MMKV : magasin clé‑valeur rapide avec chiffrement optionnel. Idéal pour les feature flags, l’état de session et de petites données métier.
- SQLite : base relationnelle (expo-sqlite ou react-native-sqlite-storage) pour des données structurées et interrogeables à grande échelle.
- Realm : base orientée objets avec API locale riche et synchronisation optionnelle ; format de fichier et migrations propres.
- WatermelonDB : ORM basé sur SQLite avec primitives de synchronisation conçues pour l’offline‑first.
- Fichiers locaux : exports JSON, documents utilisateur, pièces jointes, PDF et gros objets binaires.
- Images/médias : photos caméra, sélections galerie, médias téléchargés. Volumineux, souvent stockés hors BD, référencés par chemin ou URI.
- Identifiants sécurisés : Keychain (iOS) / Android Keystore (ou Expo SecureStore) pour les jetons, refresh tokens et clés de chiffrement.
Comparaison en un coup d’œil :
| Stockage | Usage typique | Chiffré | Compatible sauvegarde | Usage recommandé |
|---|---|---|---|---|
| AsyncStorage | Petites préférences, drapeaux, état léger | Non | Moyen | Réglages non sensibles ; n’y stockez pas de gros volumes de données |
| MMKV | Clé‑valeur rapide, drapeaux de session | Optionnel | Élevé | État d’app/session ; petites données métier ; chiffrement possible |
| SQLite | Données cœur, relations, listes | Non | Élevé | Stockage hors ligne principal avec migrations |
| Realm | Graphes d’objets complexes, réactivité | Optionnel | Élevé | Base locale riche ; Realm Sync optionnel |
| WatermelonDB | Gros jeux de données offline‑first | Non | Élevé | ORM avec primitives de synchronisation sur SQLite |
| Fichiers | Exports, docs, pièces jointes | N/A | Élevé | Sauvegarder avec manifeste, sommes de contrôle et chemins |
| Keychain/Keystore | Jetons, secrets, clés de chiffrement | Oui | Faible | Secrets liés à l’appareil ; jamais de restauration en masse entre appareils |
Notes :
- Le chiffrement Realm est opt‑in. Le chiffrement MMKV requiert de fournir/dériver une clé par magasin.
- Les magasins sécurisés sont volontairement liés à l’appareil ; prévoyez une ré‑authentification plutôt qu’une restauration de secrets.
Choisir la bonne stratégie de sauvegarde
- Export/import (fichier déclenché par l’utilisateur)
- Avantages : simple à raisonner, hors ligne, ponctuel, facile à vérifier avec des checksums.
- Inconvénients : vous devez gérer le chiffrement, les versions de schéma et les données partielles avec soin.
- Synchronisation cloud (Firebase, Supabase, API sur mesure)
- Avantages : filet de sécurité quasi continu ; le serveur est la source de vérité.
- Inconvénients : pas une sauvegarde ; suppressions et mauvaises migrations se propagent instantanément ; un point‑dans‑le‑temps reste nécessaire.
- Sauvegarde automatique de plateforme (Android Auto Backup/iCloud)
- Avantages : quasi zéro UX ; se produit automatiquement si autorisé.
- Inconvénients : peu de contrôle ou de déterminisme ; peut omettre des bases ou éléments sécurisés ; difficile à tester entre versions.
- Sauvegardes locales chiffrées (planifiées)
- Avantages : déterministes, fonctionnent hors ligne, peuvent inclure tous les magasins locaux (BD + fichiers + métadonnées).
- Inconvénients : nécessite gestion de clés, rotation, politiques de rétention et gestion de quota.
- Hybride (recommandé pour la plupart des apps)
- Avantages : continuité au quotidien via la synchro, plus export/import pour le ponctuel et la migration d’appareil.
- Inconvénients : plus d’éléments à tester et documenter.
Sauvegarde AsyncStorage
AsyncStorage convient aux petites préférences et drapeaux, mais n’est pas chiffré et n’est pas atomique sur des écritures multi‑clés. Considérez‑le comme une partie d’un plan plus vaste.
Exemple d’export avec filtrage et version de schéma :
import AsyncStorage from '@react-native-async-storage/async-storage';
import RNFS from 'react-native-fs';
const CURRENT_SCHEMA = 3;
const BACKUP_FILE = `${RNFS.DocumentDirectoryPath}/backup-async.json`;
const shouldIncludeKey = (k: string) => !k.startsWith('cache:') && !k.startsWith('tmp:');
export async function exportAsyncStorage(): Promise<string> {
const keys = (await AsyncStorage.getAllKeys()).filter(shouldIncludeKey);
const pairs = await AsyncStorage.multiGet(keys);
const data: Record<string, string | null> = Object.fromEntries(pairs);
const payload = JSON.stringify({
type: 'myapp-backup',
store: 'asyncstorage',
schema: CURRENT_SCHEMA,
ts: Date.now(),
data,
});
await RNFS.writeFile(BACKUP_FILE, payload, 'utf8');
return BACKUP_FILE;
}
export async function restoreAsyncStorage(json: string) {
const parsed = JSON.parse(json);
if (parsed.type !== 'myapp-backup' || parsed.store !== 'asyncstorage') throw new Error('Sauvegarde invalide');
if (parsed.schema > CURRENT_SCHEMA) throw new Error('Mettez l’app à jour avant de restaurer cette sauvegarde');
// Optionnel : exécuter des migrations si parsed.schema < CURRENT_SCHEMA
const entries = Object.entries(parsed.data as Record<string, string | null>)
.filter(([k]) => shouldIncludeKey(k))
.map(([k, v]) => [k, v ?? '']);
const chunk = 50;
for (let i = 0; i < entries.length; i += chunk) {
await AsyncStorage.multiSet(entries.slice(i, i + chunk) as [string, string][]);
}
}
Limites et recommandations :
- N’y stockez pas de gros ensembles de données ni de contenu utilisateur. Préférez SQLite/Realm/WatermelonDB.
- Évitez de restaurer des clés éphémères (ex. espaces de noms cache et temp).
- Ne supposez jamais que copier AsyncStorage constitue une sauvegarde React Native complète.
Sauvegarde SQLite
SQLite est idéal pour les données cœur. Les sauvegardes doivent être cohérentes même si l’app fonctionne. SQLite utilise généralement WAL (write‑ahead logging), ce qui signifie que la base peut s’étendre sur plusieurs fichiers (.db, -wal, -shm).
Bonnes pratiques :
- Point de contrôle WAL : si votre BD est ouverte et écrit, exécutez un checkpoint WAL pour purger les pages en attente.
- Vérification d’intégrité : exécutez PRAGMA integrity_check après restauration pour détecter tôt toute corruption.
- Faire correspondre la version de l’app : évitez de restaurer un schéma plus récent sur une version plus ancienne de l’app.
Exemple Expo (expo-sqlite + expo-file-system) :
import * as FileSystem from 'expo-file-system';
import * as SQLite from 'expo-sqlite';
const DB_NAME = 'app.db';
const DB_DIR = `${FileSystem.documentDirectory}SQLite`;
const DB_PATH = `${DB_DIR}/${DB_NAME}`;
export async function backupSQLite() {
// Optionnel : ouvrir la BD pour lancer un checkpoint
const db = SQLite.openDatabase(DB_NAME);
await new Promise<void>((resolve) => {
db.exec([{ sql: 'PRAGMA wal_checkpoint(FULL);', args: [] }], false, () => resolve());
});
const backupDir = `${FileSystem.documentDirectory}backups`;
await FileSystem.makeDirectoryAsync(backupDir, { intermediates: true });
const dest = `${backupDir}/app-${Date.now()}.db`;
// Si le mode WAL est activé, soit copier -wal/-shm également, soit faire un checkpoint avant (ci‑dessus)
await FileSystem.copyAsync({ from: DB_PATH, to: dest });
return dest;
}
export async function restoreSQLite(backupPath: string) {
// Remplacer le fichier de base (assurez‑vous qu’il n’y a pas d’écrivains actifs)
await FileSystem.copyAsync({ from: backupPath, to: DB_PATH });
// Vérification d’intégrité
const db = SQLite.openDatabase(DB_NAME);
await new Promise<void>((resolve, reject) => {
db.readTransaction((tx) => {
tx.executeSql('PRAGMA integrity_check;', [], (_tx, res) => {
const ok = res.rows.item(0).integrity_check === 'ok';
ok ? resolve() : reject(new Error('Échec de SQLite integrity_check'));
});
});
});
}
Options supplémentaires :
- Si votre build SQLite le supporte, utilisez
VACUUM INTO 'path'pour produire un instantané compact et cohérent sans fermer la BD. - Gardez des migrations idempotentes et uniquement en avant. Après restauration, exécutez votre pipeline de migration avant d’ouvrir l’UI principale.
- En cas de corruption, jetez la BD locale et déclenchez une resynchronisation depuis le serveur. Sauvegardez souvent pour réduire la fenêtre de perte.
Notes sur Realm et WatermelonDB
- Realm : la base est un seul fichier (plus des fichiers de verrou/gestion). Pour sauvegarder, fermez ou suspendez les écritures, copiez le fichier Realm et stockez la version de schéma actuelle. Si vous chiffrez Realm, n’incluez jamais la clé dans la sauvegarde ; protégez‑la dans le magasin sécurisé et ré‑hydratez‑la seulement après authentification.
- WatermelonDB : s’appuie sur SQLite, suivez donc les recommandations SQLite. Assurez‑vous que votre file d’attente de synchro et vos migrations sont rejouables après restauration.
Sauvegarde MMKV
MMKV est un magasin clé‑valeur haute performance. Sauvegardez uniquement ce que vous possédez et pouvez recréer en toute sécurité.
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV({ id: 'app' /* encryptionKey: derive from secure store if needed */ });
export function exportMMKV() {
const keys = mmkv.getAllKeys();
const out: Record<string, unknown> = {};
for (const k of keys) {
if (k.startsWith('cache:') || k.startsWith('session:volatile:')) continue; // filtrer les éphémères
out[k] = mmkv.getString(k) ?? mmkv.getNumber(k) ?? mmkv.getBoolean(k) ?? null;
}
return JSON.stringify({ type: 'myapp-backup', store: 'mmkv', schema: 1, ts: Date.now(), data: out });
}
export function restoreMMKV(json: string) {
const parsed = JSON.parse(json);
if (parsed.type !== 'myapp-backup' || parsed.store !== 'mmkv') throw new Error('Sauvegarde invalide');
Object.entries(parsed.data as Record<string, unknown>).forEach(([k, v]) => {
if (typeof v === 'string') mmkv.set(k, v);
else if (typeof v === 'number') mmkv.set(k, v);
else if (typeof v === 'boolean') mmkv.set(k, v);
else mmkv.delete(k);
});
}
Limites et recommandations :
- Si vous chiffrez MMKV, ne sauvegardez pas la clé de chiffrement. Stockez‑la ou dérivez‑la via Keychain/Keystore après connexion.
- Préférez garder les données sensibles hors de MMKV et dans le magasin sécurisé.
Identifiants sécurisés
- iOS Keychain, Android Keystore et Expo SecureStore sont, par conception, liés à l’appareil. Considérez‑les comme non portables entre appareils.
- Ne restaurez pas simplement des jetons d’authentification sur un nouveau téléphone. Exigez une ré‑authentification ou un ré‑appairage approuvé côté serveur pour émettre de nouveaux jetons.
- Si vous chiffrez une sauvegarde locale, protégez la clé de chiffrement par un mot de passe/NIP utilisateur ou par une clé ré‑hydratée du magasin sécurisé après connexion. Ne codez jamais des clés en dur.
Applications offline‑first
Sauvegarde et synchronisation résolvent des problèmes différents. La sauvegarde capture un point dans le temps ; la synchro fait converger les données vers la cohérence.
Blocs de construction clés :
- Local‑first : l’utilisateur écrit immédiatement dans une base locale (SQLite/Realm/WatermelonDB).
- File d’attente de synchro (outbox) : liste durable des mutations à pousser dès que le réseau revient ; idempotente par conception.
- Résolution de conflits : last‑writer‑wins, fusion avec serveur autoritatif, fusions au niveau des champs ou CRDT (Conflict‑free Replicated Data Types) pour les domaines collaboratifs.
- Reconnexion : backoff exponentiel, jitter, et reprise depuis des curseurs/points de contrôle.
Diagramme d’architecture :
Utilisateur
↓
Base locale (SQLite / Realm / WatermelonDB)
↓
File d’attente de synchro (outbox + retry + clés d’idempotence)
↓
API (REST / GraphQL)
↓
Backend (résolution de conflits, journal d’audit, instantanés)
Synchronisation cloud
Quelques choix courants :
- Firebase/Firestore : écouteurs temps réel et cache hors ligne. Créez des endpoints d’export/snapshot côté serveur pour la restauration ponctuelle ; protégez les écouteurs clients par des horodatages serveur et de la pagination.
- Supabase/Postgres : modèle relationnel avec RLS (Row‑Level Security). Utilisez la réplication logique ou la capture de changements pour piloter une synchro sélective ; exposez des API d’export par utilisateur pour la reprise.
- REST/GraphQL sur mesure : définissez des mutations idempotentes (p. ex. avec des IDs d’opération), des reprises avec backoff exponentiel, et une UI optimiste consolidée par la réconciliation serveur. Conservez des instantanés serveur pour les retours arrière.
Dans tous les cas :
- Considérez la synchro comme la commodité du quotidien, pas la reprise après sinistre.
- Versionnez vos formats « wire » et vos migrations en parallèle de votre schéma local.
- Gardez les suppressions « soft » ou tombstonées pour permettre des fenêtres de récupération.
Export/import utilisateur
Donnez aux utilisateurs la main pour sauvegarder et transporter leurs données. Un flux pratique :
- Export
- Créez un manifest.json avec : app id, user id (haché si nécessaire), versions de schéma, createdAt, plateforme, et une liste d’artéfacts (bd, fichiers) avec tailles et SHA‑256.
- Rassemblez les artéfacts : BD SQLite (ou fichier Realm), dump MMKV/AsyncStorage, et fichiers référencés (pièces jointes, images) sous une structure déterministe.
- Zipez et chiffrez l’archive.
- Chiffrer
- Dérivez une clé 256 bits à partir d’une phrase secrète avec PBKDF2 ou scrypt (stockez le sel et les itérations dans l’en‑tête).
- Chiffrez le zip avec AES‑GCM (nonce 96 bits aléatoire ; stockez le nonce dans l’en‑tête). Incluez des AAD avec app id + user id + schéma pour la détection de manipulation.
- Partager
- Utilisez la feuille de partage native pour envoyer l’archive chiffrée vers un drive cloud ou la conserver sur l’appareil.
- Importer
- Vérifiez l’en‑tête (type/app/schéma), déchiffrez, validez les checksums, puis restaurez chaque magasin (BD, fichiers, KV) dans l’ordre correct. Exécutez les migrations.
Exemple minimal de partage :
import RNFS from 'react-native-fs';
import { Share } from 'react-native';
export async function shareBackup(filePath: string) {
const exists = await RNFS.exists(filePath);
if (!exists) throw new Error('Sauvegarde introuvable');
await Share.share({ url: `file://${filePath}`, message: 'Sauvegarde chiffrée MyApp' });
}
Squelette pour manifest.json :
{
"type": "myapp-backup",
"version": 1,
"appId": "com.example.myapp",
"user": "u_12345",
"schema": { "db": 12, "mmkv": 1, "async": 3 },
"createdAt": 1731242112345,
"artifacts": [
{ "path": "db/app.db", "sha256": "...", "bytes": 1234567 },
{ "path": "kv/mmkv.json", "sha256": "...", "bytes": 2345 },
{ "path": "kv/async.json", "sha256": "...", "bytes": 1234 }
]
}
Versionnage
- Version de schéma : incrémentez lorsque vous modifiez la forme des tables/collections ou les index.
- Migrations : gardez des scripts idempotents et uniquement en avant. Testez‑les sur des instantanés de vraies données.
- Rétrocompatibilité : ne restaurez pas une sauvegarde avec un schéma plus récent que ce que l’app prend en charge. Demandez à l’utilisateur de mettre à jour.
- Compatibilité avant : pour les magasins JSON, laissez passer des champs inconnus si c’est sûr, mais n’en déduisez pas la sémantique.
- Compatibilité manifeste : incluez minAppVersion et targetPlatform pour que l’app puisse refuser proprement et guider l’utilisateur.
Reprise après sinistre
Un scénario réaliste, centré utilisateur :
Perte du téléphone → installation de l’app → authentification → téléchargement de la sauvegarde chiffrée (depuis le cloud de l’utilisateur ou un bucket de l’app) → l’app déchiffre l’archive avec une phrase secrète ou une clé réémise par le serveur → restauration de la base locale et des fichiers → exécution des migrations → reprise de la synchro pour rattraper les diffs restants du serveur → vérifications d’intégrité et confirmation visuelle.
Liste de contrôle pour ce flux :
- Verrouillez l’UI critique derrière un état « en récupération » jusqu’à la fin de la restauration.
- Fournissez des barres de progression et l’option de réessayer ou d’annuler proprement.
- Validez chaque artéfact avec un checksum avant application.
- Filet de sécurité : si la restauration de la BD échoue, autorisez une resynchronisation propre depuis le serveur.
Sécurité
- Chiffrement : utilisez AES‑GCM avec nonces aléatoires et en‑têtes authentifiés. N’utilisez jamais d’IV statiques. Stockez le nonce, le sel et les paramètres KDF dans l’en‑tête de sauvegarde, jamais les secrets.
- Mots de passe et clés : dérivez des clés d’une phrase secrète (PBKDF2/scrypt/Argon2). Protégez les clés long terme dans Keychain/Keystore. Ne codez ni ne distribuez jamais des clés dans l’app.
- Contrôle d’accès : demandez biométrie ou code appareil avant les opérations d’export/import qui exposent des données sensibles.
- Minimisation des PII : excluez caches, miniatures, jetons push et IDs analytics des sauvegardes.
- Vie privée et conformité : offrez export et effacement à l’initiative de l’utilisateur. Soyez explicite sur ce qui est sauvegardé et pendant combien de temps. Permettez la révocation des sauvegardes cloud si vous utilisez un bucket géré par l’app.
Tester la sauvegarde et la restauration
Automatisez et répétez. A minima :
- Tests unitaires : export et restauration de petits jeux de données pour AsyncStorage/MMKV.
- Tests d’intégration : créez un dataset réaliste SQLite/Realm (10k–100k lignes), exportez, effacez, restaurez, exécutez les migrations, puis lancez PRAGMA integrity_check.
- Tests de corruption : tronquez des archives et altérez des checksums pour vérifier que l’app s’arrête proprement avec des erreurs exploitables.
- Restaurations inter‑versions : restaurez des sauvegardes créées par les versions N‑1 et N‑2 de l’app.
- Cross‑platform : assurez‑vous que votre format d’archive (zip + JSON) est neutre plateforme. Ne dépendez pas des chemins OS de l’appareil source.
- Tests de performance : mesurez les durées d’export/import sur des appareils Android d’entrée de gamme. Chiffrez/compressez en flux pour éviter les pics mémoire.
Erreurs fréquentes
- Sauvegarder caches et répertoires temporaires comme s’il s’agissait de données utilisateur.
- Restaurer des schémas incompatibles sans migrations.
- Copier seulement AsyncStorage et appeler cela une « sauvegarde ».
- Inclure des jetons, refresh tokens ou clés de chiffrement dans le fichier de sauvegarde.
- Supposer que la synchro cloud équivaut à une sauvegarde ponctuelle.
- Ne jamais pratiquer une restauration complète sur de vrais appareils.
- Restaurer une BD plus récente dans une app plus ancienne sans contrôles ni garde‑fous.
Performance
- Compression : zip réduit l’I/O pour le texte et les pages SQLite. Évitez de recomprimer des médias déjà compressés.
- Surcharges de chiffrement : chiffrez en flux (par blocs) pour éviter de garder toute l’archive en mémoire.
- Sauvegardes incrémentales : n’incluez que les pages BD ou fichiers modifiés avec un manifeste roulant ; envisagez des instantanés cadencés (ex. quotidien) plus des diffs.
- Gros médias : stockez uniquement les métadonnées dans la BD ; sauvegardez les fichiers référencés sous un dossier dédié. Envisagez des options d’export séparées ou des quotas.
- Tâches d’arrière‑plan : planifiez avec WorkManager (Android), BGProcessingTask/BGAppRefreshTask (iOS), ou Headless JS si autorisé. Respectez batterie/économie de données.
Architecture de production
Un design pragmatique de bout en bout :
App React Native
↓
Base locale (SQLite/Realm) + KV (MMKV/AsyncStorage)
↓
Sauvegarde chiffrée (zip + manifeste + checksums)
↓
Stockage cloud (contrôlé par l’utilisateur ou bucket d’app)
↓
API backend (auth, registre d’instantanés, URLs signées)
↓
Restauration (valider → déchiffrer → integrity_check → migrer)
↓
Synchronisation (reprendre l’outbox, réconcilier l’état serveur)
Traits clés :
- Manifeste unique régissant tous les artéfacts et versions.
- Chiffrement authentifié : sépare qui peut lire d’où c’est stocké.
- Sauvegardes observables : émettez de la télémétrie sur succès/échec pour la supportabilité.
Liste de bonnes pratiques
- Chiffrez chaque sauvegarde avec chiffrement authentifié (AES‑GCM), nonce unique et paramètres KDF stockés.
- Conservez des versions de schéma et des migrations testées, uniquement en avant, pour la BD et les magasins JSON.
- Fournissez un flux visible d’export/import avec progression et validation d’intégrité.
- Séparez données et secrets ; ne restaurez pas des jetons entre appareils. Ré‑authentifiez.
- Exercez une restauration complète en CI et sur de vrais appareils à chaque version.
- Excluez caches/fichiers temporaires des sauvegardes ; incluez seulement les données durables.
- Implémentez l’hybride : synchro cloud pour la continuité ; export/import pour la récupération ponctuelle.
- Surveillez les échecs de restauration, les résultats de PRAGMA integrity_check et les erreurs de migration.
- Publiez une documentation de récupération conviviale avec captures d’écran.
Conclusion
La sauvegarde en React Native est avant tout un problème d’architecture applicative, pas une fonctionnalité du framework. Les solutions les plus fiables combinent le bon stockage local (SQLite/Realm/WatermelonDB pour les données ; MMKV/AsyncStorage pour l’état léger), des exports sécurisés et versionnés, une gestion prudente des identifiants sécurisés, et une synchronisation cloud pour la continuité au quotidien. Placez le chiffrement, le versionnage de schéma et des procédures de restauration déterministes au centre, et répétez régulièrement la reprise après sinistre pour que remettre les utilisateurs sur pied devienne une routine, pas une surprise.