Where does React Native Async Storage v3 keep data on iOS, Android and the web, and what does its lack of encryption rule out?
answer
- one database per instance name
- SQLite file in the app sandbox
- IndexedDB on the web
- plaintext at rest, no library encryption
- secrets belong in secure storage
basics
~20 sAsync Storage v3 writes each instance to its own plaintext SQLite file inside the app's sandbox on iOS and Android, and to an IndexedDB database on the web. Nothing is encrypted by the library, so tokens, passwords and personal data do not belong there.
solid answer
~40 sEach `createAsyncStorage(name)` instance becomes its own SQLite database: under `Application Support/async-storage/databases/<name>/` on iOS and in the app's internal files directory on Android. On the web the same name becomes an IndexedDB database. The library adds **no encryption**; the only protection is what the operating system gives every app file, mainly the sandbox that keeps other apps out. Anyone who can read the app's files, on a rooted or jailbroken device, from a debuggable build, or through script access on a web origin, reads the values as plain text. So it is right for an onboarding flag, a language or a theme, and wrong for access tokens, refresh tokens, passwords or personal data, which go to Keychain- or Keystore-backed secure storage.
go deeper
Recall that Async Storage is unencrypted and is for small non-sensitive values such as a flag or a language, never for tokens or passwords.
Explain where each instance lives per platform (SQLite files, IndexedDB on the web) and why the sandbox is the only barrier the data has.
Name the concrete exposure paths (rooted device, debuggable build, same-origin script) and audit existing keys for anything that should move to Keychain- or Keystore-backed storage.
Set a data-classification rule for the team: which categories may live in unencrypted app storage, which need secure storage, and how reviews catch drift.
## One database per instance In version 3, `createAsyncStorage(databaseName)` does not add a prefix to keys in one shared file. The name selects a **separate database**, and the library's database-naming guide spells out where it lives: | Platform | Backend | Location | |---|---|---| | iOS, macOS | SQLite | `<Application Support>/async-storage/databases/<name>/<name>.sqlite` | | Android | SQLite (through Room) | `<App Files Dir>/async-storage/databases/<name>/<name>.sqlite` | | Web | IndexedDB | a database called `<name>` | | Windows, visionOS | legacy v2 storage | one storage for the whole app | So `createAsyncStorage("user-1234")` on Android creates `.../async-storage/databases/user-1234/user-1234.sqlite`. The per-name layout is what makes scoped storages isolated: `clear()` on one instance deletes rows from one file only. Inside, the store is a single table of `key` and `value` text columns. On the web the package's default export is different again: the legacy implementation there sits on `window.localStorage`, while named instances use IndexedDB. ## What "unencrypted" means in practice The README describes Async Storage as an **unencrypted** key-value store, and the source has no encryption layer. Values sit in the SQLite file exactly as the app wrote them. Protection comes only from outside the library: - **The app sandbox.** On an unmodified phone, other apps cannot open your app's files. That is the main barrier. - **The operating system's own at-rest protection**, which applies to every app file equally and is not something Async Storage configures. Those barriers do not help when: 1. The device is **rooted or jailbroken**, or a user or attacker has file-system access. 2. The build is **debuggable**, so development tools can pull the app's data directory. 3. On the **web**, any script running on the same origin, including injected script, can open the IndexedDB database. In each case the reader sees plain text, with no key needed. ## What belongs there and what does not Good fits are small, non-sensitive values whose leak would not hurt anyone: - onboarding completed, dismissed tips, last-seen changelog version - preferred language, theme, units - a small cache the app can rebuild from the server Poor fits are anything that grants access or identifies a person: - access and refresh tokens, session cookies, API keys - passwords or PIN codes - personal or financial data a regulator would care about For secrets, mobile platforms offer **hardware-backed secure storage** (the iOS Keychain and the Android Keystore), reached from React Native through a secure-storage library such as `expo-secure-store`. How to store and rotate credentials there is a separate topic. ## Consequences of the SQLite backend The SQLite backing is an implementation detail, but it explains some behaviour: - Batch writes run inside a **transaction**, which is why `setMany` is all-or-nothing. - Reads and writes cross from JavaScript to native code through a Turbo Module, so every method is asynchronous. - It is still a key-value API: there are **no queries, indexes or partial updates** exposed to JavaScript, even though a database sits underneath. ## Using instances to limit exposure Scoped storages also help with data hygiene. Keeping per-user values in an instance named after the user, for example `createAsyncStorage("user-" + userId)`, separates them from app-wide values such as the language. On sign-out the app can call `clear()` on the user instance and know that nothing belonging to that user remains in that store, while the device-wide preferences survive. On a shared family tablet that is the difference between a clean hand-over and the next person seeing the previous user's recent searches. The same idea applies to audits. `getAllKeys()` on each instance lists exactly what the app keeps there, which makes a periodic review of "is anything sensitive in here?" a short script rather than an archaeology project. ## A common interview trap Candidates sometimes reason that "it is a database inside the sandbox, so it is safe". The sandbox answers the question "can another app read it?", not "can someone with the device, a backup or a debug build read it?". For an onboarding flag nobody cares. For a refresh token it is the difference between a stolen phone and a stolen account.
- The Android app sandbox stops other apps reading the file. Why is that not enough for a refresh token?The sandbox only stops other apps on an unmodified device. A rooted phone, a debuggable build or an extracted copy of the app's data exposes the SQLite file, and the token is plain text inside it. Secure storage keeps the secret encrypted with a key held by the platform's Keystore, so a copied file alone is not enough.
- Where does a named Async Storage v3 instance live in a React Native Web build?In an IndexedDB database whose name is the instance name. The package's default export on the web is the legacy implementation over `window.localStorage`, so the two write to different browser stores. Either way any script on the same origin can read the values.
Async Storage is a labelled shoebox in your own bedroom: other people in the house (other apps) are not allowed in, but anyone who does get into the room can open the box and read every note, because the box itself has no lock.
saying these in an interview costs you the question
- Assuming Async Storage encrypts values because they sit in a SQLite file
- Saying the app sandbox makes stored tokens safe on any device
- Keeping refresh tokens or passwords in Async Storage for convenience
- Believing instances share one file and only prefix their keys
- Thinking the web build uses localStorage for named v3 instances