An Expo app keeps small key/value data in one of three places: AsyncStorage, MMKV, or expo-sqlite/kv-store. For most apps, pick expo-sqlite/kv-store. It runs in Expo Go, has the same API as AsyncStorage, and adds synchronous calls on native. Pick MMKV when you already need a development build and reads are on a hot path. Keep AsyncStorage for a library that requires it. Tokens and passwords belong in expo-secure-store, whichever store you choose for the rest.
The comparison in one table
| AsyncStorage | MMKV | expo-sqlite/kv-store | |
|---|---|---|---|
| Runs in Expo Go | Yes | No | Yes |
| Sync API | No, async only | Yes, every call | Yes on native; async only on web in ReadyNative |
| Relative speed | The async baseline | Fastest; its README claims about 30x AsyncStorage | SQLite-backed; Expo publishes no numbers |
| Encryption | None; Expo calls it unencrypted | Optional, with an encryptionKey | Not described in Expo's docs |
| Native build needed | No | Yes, a development build | No |
expo-sqlite/kv-store: AsyncStorage's API on SQLite
expo-sqlite/kv-store is Expo's drop-in replacement for AsyncStorage. Expo documents its Storage object as having the same API as AsyncStorage, backed by SQLite, and expo-sqlite is included in Expo Go. Switching an app over is an import change:
- import AsyncStorage from "@react-native-async-storage/async-storage";
+ import AsyncStorage from "expo-sqlite/kv-store";The extra is the synchronous calls. On native, setItemSync, getItemSync and removeItemSync return at once, which is why ReadyNative's built-in stores can hydrate before first paint:
import Storage from "expo-sqlite/kv-store";
Storage.setItemSync("theme", "dark");
const theme = Storage.getItemSync("theme"); // "dark", or null when unsetWeb is the exception. Expo's SQLite on web needs Metro set up for wasm files and the COOP and COEP headers for SharedArrayBuffer, so ReadyNative's adapter exposes only the async methods there.
Switching does not move existing data. AsyncStorage keeps its own store, so values already written stay there until you copy them. The FAQ below has the loop.
MMKV: the fastest store, with a development build
react-native-mmkv 4 is a synchronous, memory-mapped key/value store built on Nitro modules. Its calls return at once, with no Promise, and its README claims about 30 times the speed of AsyncStorage on the author's benchmark. It is native code, so Expo Go cannot load it. Make a development build with npx expo run:ios or npx expo run:android, or with bunx eas-cli build --profile development, and read Expo Go vs development build for why.
import { createMMKV } from "react-native-mmkv";
export const storage = createMMKV({ id: "readynative" });
storage.set("theme", "dark");
storage.getString("theme"); // "dark"Encryption is opt-in. By default MMKV writes plain text and relies on the iOS and Android app sandbox. Pass an encryptionKey to createMMKV and the data is encrypted with AES-128, or with AES-256 when you also set encryptionType to AES-256. Credentials still belong in expo-secure-store, which uses the Keychain and the Android Keystore.
MMKV has its own AsyncStorage migration guide: a one-time loop that copies each key and removes it from AsyncStorage afterwards.
expo-secure-store: secrets, not a general store
expo-secure-store encrypts small key/value pairs on the device. On Android the values go in SharedPreferences, encrypted with the Android Keystore system; on iOS they go in the Keychain. It is included in Expo Go, and its API is async:
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("refresh_token", token);
const saved = await SecureStore.getItemAsync("refresh_token");
await SecureStore.deleteItemAsync("refresh_token");Keys may contain letters, digits, dots, dashes and underscores. Expo enforces no size limit, but some older iOS releases refused values above roughly 2048 bytes, so store a token there, not a document. On iOS the Keychain also keeps data across a reinstall with the same bundle id, which Expo says not to rely on.
ReadyNative uses it where a session lives, not in the storage module. The Clerk module keeps its client JWT in SecureStore through Clerk's token cache, and the Better Auth module keeps its session cookie there. The auth comparison lists where each option keeps its session. None of the three storage options touch it.
When each one wins
- expo-sqlite/kv-store: most apps. Expo Go works, reads are synchronous on native, and it adds no native library beyond expo-sqlite.
- MMKV: you already make a development build for push notifications, purchases or widgets, and the app reads storage often enough that speed matters.
- AsyncStorage: a library you use requires it as a storage option, such as the cache persister in TanStack Query. Keep one store for the whole app when you can.
- expo-secure-store: alongside any of the three, for tokens and passwords only.
How ReadyNative wires it
The storage category has three options, and each one writes the same file, src/lib/storage.ts. Your code imports storage from @/lib/storage and talks to the StorageAdapter interface in src/lib/storage-adapter.ts. The built-in stores, zustand persist, Jotai and i18n all go through that adapter, so they do not change when you switch. The default is kv-store.
bun run setup --storage mmkvThat replaces src/lib/storage.ts and nothing else. Keys stay the same readynative:* strings, but the data lives in the new backend, so existing values do not move and users start fresh. The template has no migration, so copy any data you need first. The option pages are on the kv-store stack page.
Read next: Expo Go vs development build, for why MMKV needs a build, then the storage module docs.
FAQ
Is expo-sqlite/kv-store a drop-in replacement for AsyncStorage?
Expo documents it that way: Storage has the same API as AsyncStorage, so for most code you change only the import. It also adds synchronous calls, which ReadyNative exposes on native and not on web.
Does MMKV work in Expo Go?
No. react-native-mmkv is a native Nitro module, and Expo Go does not load it. Make a development build with npx expo run:ios, npx expo run:android or bunx eas-cli build --profile development, and run the app from that build.
Is AsyncStorage encrypted?
No. Expo describes AsyncStorage as unencrypted. MMKV encrypts only when you pass an encryptionKey, and expo-secure-store uses the Keychain and the Android Keystore, so put tokens and passwords there.
How do I move existing AsyncStorage data to expo-sqlite/kv-store?
Changing the import does not copy old values; they stay in AsyncStorage. Run a one-time loop while both imports exist: for each key from AsyncStorage.getAllKeys(), read AsyncStorage.getItem(key) and write it with Storage.setItemAsync(key, value).
How large can a SecureStore value be?
Expo does not enforce a limit, but some older iOS releases refused values above roughly 2048 bytes. Keep tokens small, and handle the rejection that setItemAsync returns when a value cannot be stored.
More guides
- Unistyles 3 with Expo: a working setup
- Expo environment variables: which are public?
- TanStack Query in Expo: offline and refetch
- Expo zustand: persist a store across restarts
- jest-expo setup, Expo Router tests and fixes
- Expo auth: Clerk vs Supabase vs Better Auth
- Expo i18n: i18next or Lingui, compared
- Expo android widgets: expo-widgets on SDK 58
- Tamagui in Expo: setup, styling and themes
- Best paywall tools for Expo in 2026
- Expo widgets on iOS: a working setup
- RevenueCat subscriptions in an Expo Router app
- Supabase auth in Expo: email, Apple and Google
- Expo push notifications: setup, token, testing
- Expo Go vs development build: limitations
- The best Expo boilerplates, compared