The short answer
Pick backup-excluded storage with re-sync on fresh installs, and treat your checksum/version marker as an integrity check, not a provenance check. The reason is that neither AsyncStorage nor MMKV can tell you where bytes came from — platform backups restore bytes, not meaning. Since you must implement provenance at the application layer anyway, the restore path stops being an asset and becomes a liability: it is the only path that can silently resurrect stale state. Excluding your data from backup removes that path entirely, and "fresh install" and "restored device" collapse into the same, already-tested code path: re-authenticate, re-sync.
Why a provenance marker inside backed-up data can't fully work
Your instinct to store a schema version, record count, and checksum is right for detecting corruption and version skew. But provenance — "was this written by this install, for this user, on this device?" — requires an anchor that does not travel with the backup. Anything stored next to the payload (device ID, install timestamp, user ID) gets restored along with it and proves nothing. The only reliable anchors are:
- A server-side epoch. The server issues a per-user data generation or session version; the client stores it and presents it on launch. A restored client presents an old epoch, the server rejects or forces re-sync. This is the only cross-platform mechanism that genuinely distinguishes "restored" from "current," because the truth lives off-device.
- Keystore-held secrets. Android Keystore keys generally do not survive backup/restore, so data encrypted with a keystore key is self-invalidating on a new device. iOS Keychain items can persist across restores to the same account, so behavior differs by platform — verify against the versions you ship.
Notice both anchors work independently of whether the payload sits in AsyncStorage or MMKV. That's the core finding: the storage engine is not the decision that determines verifiability.
So why exclude backup at all?
Because backup-inclusive storage gives you a second, worse failure mode with no compensating benefit. If your server-epoch check is mandatory anyway, a successful restore saves you one re-sync of preferences and a small cache — cheap data — while a failed-to-detect restore is exactly the silent-stale-state bug you're designing against. Also note Android Auto Backup has a per-app data cap (historically ~25 MB) and restores can be silently skipped, so you can't even rely on restore being complete. Asymmetry of risk says: opt out.
Concrete shape of the solution
- Set
android:allowBackup="false" or use fullBackupContent/dataExtractionRules to exclude your storage files; on iOS, mark the relevant files/directories to be excluded from backup. - Keep the schema version + checksum for corruption detection (MMKV's built-in CRC covers file damage but not semantic staleness or cross-user restore, so keep your own check).
- On cold start, validate locally, then confirm the server epoch before trusting anything. Mismatch → wipe, re-authenticate, re-sync.
- Auth tokens: never trust restored tokens. Always re-validate against the server on launch, and prefer short-lived tokens with refresh. A restored token is indistinguishable from a stolen one.
What to verify before shipping
Do a real end-to-end test: back up a debug build, restore to a second device or emulator, and log what your validation layer reports on first launch. Confirm the backup exclusion actually took effect (inspect the restored sandbox), and add a production metric for "restored data failed validation" so you know how often the recovery path fires. If you later decide you want seamless restore for non-sensitive preferences, you can layer it back on top of the epoch check — but build the verified path first.