When a user leaves a one-star review saying "I opened the app on my new phone and everything was gone," the cause is almost always something you forgot to configure around iCloud backup. A naively built Rork app frequently fails to carry AsyncStorage data and Keychain tokens across a device migration, which means you lose retained users every time they upgrade their phone.
I have hit this exact bug across three different apps. It never reproduces during local debugging, slips past TestFlight, and only surfaces in the production review section — exactly the kind of issue you want to kill before shipping. This guide explains where a Rork app's data actually lives, how each location behaves on restore, and the configuration changes that keep state intact for the user.
Why Rork apps lose data after an iCloud restore
iOS storage locations behave differently when it comes to iCloud backup inclusion. The places a Rork app (React Native + Expo) commonly writes to break down like this.
- AsyncStorage: Internally backed by files under
Library/Application Support/RCTAsyncLocalStorage/. Included in iCloud backup by default, but some libraries silently mark the container as excluded. - Documents folder: For files the user created. Included in iCloud backup by default.
- Library/Caches: For caches. Not included in iCloud backup, and the system may delete it at any time.
- tmp/: Temporary files. Not included in iCloud backup.
- Keychain: Encrypted credential store. Carried over to a new device through iCloud Keychain — but only when
kSecAttrAccessibleis set to a non-ThisDeviceOnlyvalue.
The patterns I see most often when data disappears boil down to three.
- User-generated data was stored in
Library/Caches/. Large images or recordings placed in Caches need to be regenerated from scratch after restore. - AsyncStorage's container ended up flagged as excluded from backup. A handful of libraries do this automatically without telling you.
- Keychain items were saved with a
ThisDeviceOnlyaccessibility class. Auth tokens silently vanish on the new device.
There is also a fourth, sneakier failure mode: data lives in the right place, but the user never enabled iCloud Backup at all. You cannot fix that from the app, but you can detect it and surface a friendly nudge in the onboarding flow. That alone often improves your effective restore rate noticeably.
Let's walk through each fix in order.
Start by making your storage locations visible
You cannot reason about restore behavior until you actually see where everything lives. Drop this snippet in early during app startup the next time you build for a physical device.
import * as FileSystem from 'expo-file-system';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Call once on app launch, in dev builds only.
export async function debugStorageLocations() {
console.log('documentDirectory:', FileSystem.documentDirectory);
console.log('cacheDirectory:', FileSystem.cacheDirectory);
console.log('bundleDirectory:', FileSystem.bundleDirectory);
const keys = await AsyncStorage.getAllKeys();
console.log('AsyncStorage keys count:', keys.length);
console.log('AsyncStorage keys:', keys);
}On a real iOS device you will see something like this in the logs.
documentDirectory: file:///var/mobile/Containers/Data/Application/<UUID>/Documents/
cacheDirectory: file:///var/mobile/Containers/Data/Application/<UUID>/Library/Caches/
AsyncStorage keys count: 12
If anything that the user perceives as "their data" — photos they took, audio they recorded, notes they typed — lives under Library/Caches/, that is a design bug. Move it to Documents/ or Library/Application Support/ and ship a one-time migration step that copies existing files to the new location and deletes the originals.
A practical rule of thumb: data the user thinks of as theirs goes in Documents, internal app state and master data goes in Application Support, and only regenerable caches belong in Caches. The Application Support directory is hidden from the iOS Files app, which is exactly what you want for "internal stuff users do not need to touch."
Mark each file's backup status explicitly
iOS lets you control backup inclusion at the per-file level. Skip this and a single multi-gigabyte video file in Documents will balloon your users' iCloud bills, leading to angry "this app eats my storage" reviews.
The opposite mistake is just as bad: real user data with the Excluded from backup flag set silently disappears on restore. In Expo / React Native you control this through expo-file-system.
import * as FileSystem from 'expo-file-system';
// Exclude from backup (cached thumbnails you can re-download)
await FileSystem.setExcludeFromBackupAsync(
`${FileSystem.documentDirectory}cached_thumbnails/photo_001.jpg`
);
// Include in backup (handwritten notes the user owns)
// → Default behavior. There is no explicit "include" call.
// If you previously called setExcludeFromBackupAsync on a path,
// copy it to a fresh path so the flag is reset.Expected behavior: any file you pass to setExcludeFromBackupAsync is dropped from iCloud backup and will not reappear on a restored device. Anything you do not pass is backed up by default.
The case that bit me hardest was a thumbnail cache I had quietly written to Documents. The originals were re-fetchable from a CDN, so the right home was Caches in the first place — and on top of that I had forgotten the exclude flag, so users were paying iCloud storage for files I could regenerate for free. Always classify a file by asking "did the user create this, or can I recreate it from somewhere else?" first. If the answer is the latter, it belongs in Caches and never needs the exclude flag at all.
If you must keep regenerable assets in Documents (perhaps because the user can browse them in the iOS Files app), tag each one with setExcludeFromBackupAsync immediately after writing it. Make the flag application a wrapper around your write helper so it is impossible to forget.
Pick a Keychain accessibility class that survives migration
If you stash auth tokens in the Keychain, the value of kSecAttrAccessible decides whether the user has to log in again on their new phone. Here is the difference using expo-secure-store.
import * as SecureStore from 'expo-secure-store';
// ❌ Bad: ThisDeviceOnly classes never migrate
await SecureStore.setItemAsync('auth_token', token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
// ✅ Good: AFTER_FIRST_UNLOCK rides over via iCloud Keychain
await SecureStore.setItemAsync('auth_token', token, {
keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
});Expected behavior with AFTER_FIRST_UNLOCK: the moment the user unlocks the new device for the first time, any account where iCloud Keychain is enabled under the same Apple ID will see the auth token reappear, and your app stays logged in.
That said, some token types genuinely should not migrate. A payment-grade token gated behind Face ID is usually safer with WHEN_PASSCODE_SET_THIS_DEVICE_ONLY. Decide as a product question — "do I want this user to re-authenticate on a new device, or quietly stay signed in?" — before you pick the accessibility class. I cover the broader Keychain story in the complete guide to storing secrets safely with Rork and Keychain if you want to go deeper.
One subtle gotcha: if you change a Keychain item's accessibility class in a later release, existing items keep their old class. You either need to delete and rewrite the item the next time the user signs in, or run a one-time migration during app startup that reads the value, deletes the entry, and re-saves it with the new class.
Pre-release test: actually restore from iCloud on a real device
Once you have fixed the configuration, you have to verify it on hardware. The simulator does not faithfully reproduce iCloud restore, so you need a physical device, an Apple ID, and a bit of patience.
- Take an iCloud backup on test device A.
- Settings → Apple ID → iCloud → iCloud Backup → Back Up Now
- Wait until "Last Backup" reads "Just now"
- Erase test device B (or wipe device A) and restore.
- Settings → General → Transfer or Reset iPhone → Erase All Content and Settings
- During setup, choose "Restore from iCloud Backup" and select the backup from step 1
- Open the app and visually verify what survived.
- Is the user still logged in?
- Is user-generated content visible?
- Were downloaded files restored, or — if not — does the app correctly prompt the user to re-download them?
Anything missing points back to the storage location, the backup flag, or the Keychain accessibility class. Date and timezone glitches also tend to surface on restored devices, so it is worth pairing this test with the patterns in fixing date and timezone bugs in Rork apps when times are nine hours off.
If you do not have a spare device, the cheapest reproducible alternative is to use the same device, take the backup, then erase and restore. It costs you the better part of an evening but gives you the only signal that actually matters before App Store reviewers — or worse, paying users — find the bug for you.
A note on Android parity
Android has a similar story but with different defaults. react-native-async-storage writes to the app sandbox, which is included in Auto Backup since Android 6. If you opted out of Auto Backup in your AndroidManifest.xml (android:allowBackup="false"), the same kind of restore-zero issue is waiting for you on Android. Most Rork developers never touch that flag, so the default is safe — but if you copied a manifest snippet from a security-hardening blog post early on, double-check what is in there before you ship.
For truly large user-generated assets like photos and videos, neither iCloud nor Google Drive is a great answer at scale. Keep the metadata in AsyncStorage / SQLite, and offload the binary blobs to your own backend or a service like Supabase Storage. That way restore brings back the references and the user simply re-downloads what they need.
Closing thought — verify restore behavior before every release
A device migration is the moment a user decides whether to keep using your app or quietly walk away. If everything resets to zero at that moment, even a beautifully built app earns a one-star review on the spot.
The single most useful action you can take today is to call debugStorageLocations in your own app and read the output. If you find anything that the user owns sitting under Library/Caches/, fixing just that path will resolve most of the restore failures you currently ship with. While you are at it, double-check the kSecAttrAccessible value in your Keychain calls — it is the kind of setting nobody touches after the first day of development, and it quietly costs you returning users.