## Introduction

React Native does not provide backup for you. Reliable backup and restore is an application architecture decision that depends on where you store data, how you encrypt it, what each platform supports, and how your sync model works. In production, “React Native backup” is the difference between users keeping their history and starting from zero after a device loss.

Four concepts to keep straight:

- Local backup: a point‑in‑time snapshot created on the device (usually a file or archive) that you can export or restore even while offline.

- Cloud synchronization: continuous replication to a backend. This is not a substitute for a point‑in‑time backup; deletes and bugs propagate.

- Disaster recovery: a tested path to get a user back to a working state after loss, theft, or corruption.

- Device migration: safely moving a user to a new phone without leaking credentials.

Different storage technologies require different approaches. Never imply that copying AsyncStorage alone is a complete backup strategy.

## Where React Native stores data

Common storage layers in React Native and what they typically hold:

- AsyncStorage: small key‑value prefs and metadata. Not encrypted by default. Not transactional across multiple writes.

- MMKV: fast key‑value store with optional encryption. Great for feature flags, session state, and small domain data.

- SQLite: relational database (expo-sqlite or react-native-sqlite-storage) for queryable, structured data at scale.

- Realm: object database with a rich local API and optional sync; its own file format and migrations.

- WatermelonDB: SQLite‑backed ORM with sync primitives designed for offline-first.

- Local files: JSON exports, user documents, attachments, PDFs, and large binary objects.

- Images/media: camera photos, gallery picks, downloaded media. Large and often stored outside the DB, referenced by path or URI.

- Secure credentials: Keychain (iOS) / Android Keystore (or Expo SecureStore) for tokens, refresh tokens, and encryption keys.

Comparison at a glance:

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Storage</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Typical use</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Encrypted</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Backup friendly</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Recommended use</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">AsyncStorage</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Small prefs, flags, lightweight state</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">No</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Medium</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Non-sensitive settings; do not store large datasets</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">MMKV</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Fast key‑value, session flags</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Optional</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">High</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">App/session state; small domain data; optionally encrypted</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">SQLite</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Core domain data, relations, lists</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">No</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">High</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Primary offline store with migrations</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Realm</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Complex object graphs, reactivity</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Optional</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">High</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rich local DB; optional Realm Sync</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">WatermelonDB</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Large offline-first datasets</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">No</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">High</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">ORM with sync primitives over SQLite</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Files</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Exports, docs, attachments</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">N/A</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">High</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Back up with manifest, checksums, and paths</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Keychain/Keystore</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Tokens, secrets, encryption keys</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Yes</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Low</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Device-bound secrets; never bulk-restore between devices</td></tr></tbody></table></div>
Notes:

- Realm encryption is opt‑in. MMKV encryption requires providing/deriving a key per store.

- Secure stores are intentionally device‑bound; plan to re‑authenticate instead of restoring secrets.

## Choosing the right backup strategy

- Export/import (user‑initiated file)

- Pros: simple to reason about, offline, point‑in‑time, easy to verify with checksums.

- Cons: you must handle encryption, schema versions, and partial data carefully.

- Cloud synchronization (Firebase, Supabase, custom API)

- Pros: near‑continuous safety net; server is the source of truth.

- Cons: not a backup; accidental deletes and bad migrations replicate instantly; still need point‑in‑time recovery.

- Automatic platform backup (Android Auto Backup/iCloud)

- Pros: almost zero UX; happens automatically when allowed.

- Cons: little control or determinism; can miss databases or secure items; hard to test across versions.

- Local encrypted backups (scheduled)

- Pros: deterministic, works offline, can include all local stores (DB + files + metadata).

- Cons: requires key management, rotation, retention policies, and quota handling.

- Hybrid (recommended for most apps)

- Pros: day‑to‑day continuity via sync, plus export/import for point‑in‑time and device migration.

- Cons: more moving parts to test and document.

## AsyncStorage backup

AsyncStorage is fine for small preferences and flags, but it is not encrypted and not atomic across multi‑key writes. Treat it as part of a larger plan.

Export example with filtering and schema versioning:

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('Invalid backup');
 if (parsed.schema > CURRENT_SCHEMA) throw new Error('Update the app before restoring this backup');

 // Optionally run migrations when 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][]);
 }
} 
 Limitations and guidance:

- Do not store large datasets or user content here. Use SQLite/Realm/WatermelonDB instead.

- Avoid restoring ephemeral keys (e.g., cache and temp namespaces).

- Never assume copying AsyncStorage is a complete React Native backup.

## SQLite backup

SQLite is ideal for core domain data. Backups must be consistent even while the app is running. SQLite typically uses WAL (write‑ahead logging), which means the database may span multiple files (.db, -wal, -shm).

Best practices:

- Quiesce or checkpoint: if your DB is open and writing, run a WAL checkpoint to flush pending pages.

- Integrity check: run PRAGMA integrity_check after restore to detect corruption early.

- Match app version: avoid restoring a newer schema onto an older app build.

Expo example (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() {
 // Optional: open DB to run 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`;

 // If WAL mode is enabled, either copy -wal/-shm as well or checkpoint first (we did above)
 await FileSystem.copyAsync({ from: DB_PATH, to: dest });
 return dest;
}

export async function restoreSQLite(backupPath: string) {
 // Replace database file (ensure no active writers)
 await FileSystem.copyAsync({ from: backupPath, to: DB_PATH });

 // Integrity check
 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('SQLite integrity_check failed'));
 });
 });
 });
} 
 Additional options:

- If your SQLite build supports it, use VACUUM INTO 'path' to produce a compact, consistent snapshot without closing the DB.

- Keep schema migrations idempotent and forward‑only. After restore, run your migration pipeline before opening the main UI.

- On corruption, discard the local DB and trigger a server‑driven resync. Back up often to reduce data loss windows.

## Realm and WatermelonDB notes

- Realm: the database is a single file (plus lock/management files). For backup, close or pause writes, copy the Realm file, and store the current schema version. If using Realm encryption, never include the key inside the backup; protect it in the secure store and re‑hydrate only after authentication.

- WatermelonDB: it rides on SQLite, so follow the SQLite guidance. Ensure your sync queue and migrations are replayable after restore.

## MMKV backup

MMKV is a high‑performance key‑value store. Back up only what you own and can safely recreate.

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; // filter ephemerals
 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('Invalid backup');
 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);
 });
} 
 Limitations and guidance:

- If you encrypt MMKV, do not back up the encryption key. Store or derive it via Keychain/Keystore after login.

- Prefer to keep sensitive data out of MMKV and in the secure store.

## Secure credentials

- iOS Keychain, Android Keystore, and Expo SecureStore are device‑bound by design. Treat them as non‑portable across devices.

- Do not simply restore authentication tokens on a new phone. Require re‑authentication or a server‑approved rebind to issue fresh tokens.

- If you encrypt a local backup, protect the encryption key with a user password/PIN or a key re‑hydrated from secure storage after login. Never hardcode keys in source.

## Offline-first applications

Backup and synchronization solve different problems. Backup captures a point in time; sync moves data toward eventual consistency.

Key building blocks:

- Local‑first: user writes immediately to a local DB (SQLite/Realm/WatermelonDB).

- Sync queue (outbox): durable list of mutations to push when network returns; idempotent by design.

- Conflict resolution: last‑writer‑wins, server‑authoritative merge, field‑level merges, or CRDTs for collaborative domains.

- Reconnect logic: exponential backoff, jitter, and resume from cursors/checkpoints.

Architecture diagram:

User
 ↓
Local Database (SQLite / Realm / WatermelonDB)
 ↓
Sync Queue (outbox + retry + idempotency keys)
 ↓
API (REST / GraphQL)
 ↓
Backend (conflict resolution, audit log, snapshots) 

## Cloud synchronization

 A few common choices:

- Firebase/Firestore: realtime listeners and offline caching. Build server‑side export/snapshot endpoints for point‑in‑time recovery; guard client listeners with server timestamps and pagination.

- Supabase/Postgres: relational model with RLS. Use logical replication or change capture to drive selective sync; expose per‑user export APIs for recovery.

- Custom REST/GraphQL: define idempotent mutations (e.g., with operation IDs), retries with exponential backoff, and optimistic UI guarded by server reconciliation. Keep server snapshots for rollbacks.

In all cases:

- Treat sync as day‑to‑day convenience, not disaster recovery.

- Version your wire formats and migrations alongside your local schema.

- Keep deletes soft or tombstoned to enable recovery windows.

## User export/import

Give users control to back up and carry their data. A practical flow:

- Export

- Create a manifest.json with: app id, user id (hashed if necessary), schema version(s), createdAt, platform, and a list of artifacts (db, files) with sizes and SHA‑256 checksums.

- Collect artifacts: SQLite DB (or Realm file), MMKV/AsyncStorage dump, and referenced files (attachments, images) under a deterministic structure.

- Zip and encrypt the archive.

- Encrypt

- Derive a 256‑bit key from a passphrase using PBKDF2 or scrypt (store salt, iterations in the header).

- Encrypt the zip with AES‑GCM (random 96‑bit nonce; store nonce in the header). Include AAD with app id + user id + schema for tamper detection.

- Share

- Use the native share sheet to send the encrypted archive to cloud drive or keep it on device.

- Import

- Verify header (type/app/schema), decrypt, validate checksums, then restore each store (DB, files, KV) in the correct order. Run migrations.

Minimal share example:

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('Backup not found');
 await Share.share({ url: `file://${filePath}`, message: 'MyApp encrypted backup' });
} 
 Skeleton for 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 }
 ]
} 

## Versioning

- Schema version: bump when you change table/collection shapes or indexes.

- Migrations: keep forward‑only, idempotent scripts. Test them on real data snapshots.

- Backward compatibility: do not restore a backup with a newer schema than the app supports. Prompt users to update first.

- Forward compatibility: for JSON stores, allow unknown fields to pass through if safe, but do not assume meaning.

- Manifest compatibility: include minAppVersion and targetPlatform so the app can cleanly reject and guide the user.

## Disaster recovery

 A realistic, user‑centric scenario:

User loses phone → installs app → authenticates → downloads encrypted backup (from user’s cloud or app bucket) → app decrypts archive using passphrase or server‑reissued key → restores local database and files → runs migrations → resumes sync to catch remaining server diffs → integrity checks and UI confirmation.

Checklist for this flow:

- Lock critical UI behind an “in recovery” gate until restore completes.

- Provide progress bars and the option to retry or cancel safely.

- Validate every artifact with checksums before applying.

- Fallback: if DB restore fails, allow a clean resync from the server.

## Security

- Encryption: use AES‑GCM with random nonces and authenticated headers. Never use static IVs. Store nonce, salt, and KDF params in the backup header, not secrets.

- Passwords and keys: derive keys from a passphrase (PBKDF2/scrypt/Argon2). Protect long‑term keys in Keychain/Keystore. Do not hardcode or ship keys in the app.

- Access control: prompt for biometrics or device passcode before export/import operations that reveal sensitive data.

- PII minimization: exclude caches, thumbnails, push tokens, and analytics IDs from backups.

- Privacy and compliance: provide user‑initiated export and erasure. Be explicit about what is backed up and for how long. Allow users to revoke cloud backups if using an app‑managed bucket.

## Testing backup and restore

Automate and rehearse. At minimum:

- Unit tests: export and restore small datasets for AsyncStorage/MMKV.

- Integration tests: create a realistic SQLite/Realm dataset (10k–100k rows), export, wipe, restore, run migrations, then run PRAGMA integrity_check.

- Corruption testing: truncate archives and tamper with checksums to verify the app aborts safely and surfaces actionable errors.

- Cross‑version restores: restore backups created by N‑1 and N‑2 app versions.

- Cross‑platform: ensure your archive format (zip + JSON) is platform neutral. Do not rely on OS paths from the source device.

- Performance tests: measure export/import durations on low‑end Android devices. Stream compress/encrypt to avoid memory spikes.

## Common mistakes

- Backing up caches and temporary directories as if they were user data.

- Restoring incompatible schemas without migrations.

- Copying only AsyncStorage and calling it a “backup”.

- Including tokens, refresh tokens, or encryption keys inside the backup file.

- Assuming cloud sync equals a point‑in‑time backup.

- Never practicing a full restore on real devices.

- Restoring a newer DB into an older app build without checks and guards.

## Performance

- Compression: zip reduces I/O for text and SQLite pages. Skip recompressing already‑compressed media.

- Encryption overhead: stream encrypt (chunked) to avoid keeping the whole archive in memory.

- Incremental backups: include only changed DB pages or files with a rolling manifest; consider time‑boxed snapshots (e.g., daily) plus diffs.

- Large media: store only metadata in DB; back up referenced files under a dedicated folder. Consider separate media export toggles or quotas.

- Background tasks: schedule with WorkManager (Android), BGProcessingTask/BGAppRefreshTask (iOS), or Headless JS where permitted. Respect battery/data saver.

## Production architecture

A pragmatic end‑to‑end design:

React Native App
 ↓
Local Database (SQLite/Realm) + KV (MMKV/AsyncStorage)
 ↓
Encrypted Backup (zip + manifest + checksums)
 ↓
Cloud Storage (user-controlled or app bucket)
 ↓
Backend API (auth, snapshot registry, signed URLs)
 ↓
Restore (validate → decrypt → integrity_check → migrate)
 ↓
Synchronization (resume outbox, reconcile server state) 
 Key traits:

- Single manifest governing all artifacts and versions.

- Authenticated encryption separates who can read from where it’s stored.

- Backups are observable: emit telemetry on success/failure for supportability.

## Best practices checklist

- Encrypt every backup with authenticated encryption (AES‑GCM), unique nonce, and stored KDF params.

- Keep schema versions and tested, forward‑only migrations for DB and JSON stores.

- Provide a user‑visible export/import flow with progress and integrity validation.

- Separate data from secrets; do not restore tokens across devices. Re‑authenticate.

- Exercise full restore in CI and on real devices for each release.

- Exclude caches/temp files from backups; include only durable user data.

- Implement hybrid: cloud sync for continuity; export/import for point‑in‑time recovery.

- Monitor restore failures, PRAGMA integrity_check results, and migration errors.

- Publish user‑friendly recovery documentation with screenshots.

## Conclusion

React Native backup is primarily an application architecture problem, not a framework feature. The most reliable solutions combine the right local storage (SQLite/Realm/WatermelonDB for data; MMKV/AsyncStorage for small state), secure and versioned exports, cautious handling of secure credentials, and cloud synchronization for day‑to‑day continuity. Put encryption, schema versioning, and deterministic restore procedures at the center, and rehearse disaster recovery regularly so getting users back on their feet is routine, not a surprise.