Using Expo SecureStore: Encrypting Sensitive Data in Managed and Bare Workflows
Securely store tokens and small secrets in Expo apps with SecureStore. Learn how it works, a step‑by‑step example, and common pitfalls to avoid.
17 Sept 2025, 07:53 UTC

Why SecureStore is the right choice for small secrets
When an app needs to persist a JWT, API key, or a short user‑specific string, the developer must choose a storage mechanism that protects the data from casual snooping. Expo’s expo-secure-store module wraps the platform’s native secure storage APIs (iOS Keychain and Android EncryptedSharedPreferences) and exposes a simple promise‑based API. It guarantees that:
- The value is encrypted at rest – the operating system handles the cryptographic key.
- Only the app’s sandbox can read the data – no other apps or background services can access it.
- The entry is automatically removed when the user deletes the app, keeping the device clean.
For anything larger than a few kilobytes, or for binary blobs, a different solution (e.g., a secure server or a dedicated key‑management service) is recommended.
Getting Started – add the dependency
In a managed Expo project the module is already bundled, but you still need to install the package so that the JavaScript bundle knows about it:
expo install expo-secure-store
In a bare workflow you must install the native module and rebuild the app:
npm install expo-secure-store
npx pod-install ios # iOS only
# Android rebuild via Gradle
After that, the module can be imported from expo-secure-store in any JavaScript file.
A concrete example – storing and retrieving a token
Below is a minimal snippet you can drop into App.js of a new project. It demonstrates the full cycle: set, get, and log the value.
import * as SecureStore from 'expo-secure-store';
async function demo() {
// Store a JWT string under the key "authToken"
await SecureStore.setItemAsync('authToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Retrieve the value later – it returns a string or null
const token = await SecureStore.getItemAsync('authToken');
console.log('Stored value:', token);
}
demo();
Run the app on a device or simulator ( expo start → open iOS/Android). The console will print the stored JWT. Close and reopen the app; the value remains because SecureStore persists data across restarts.
How it works under the hood
On iOS, the module calls SecItemAdd and SecItemCopyMatching from the Keychain Services API. The data is stored with the default access group, meaning only the app’s bundle ID can read it. On Android, the module uses EncryptedSharedPreferences with AES‑256 GCM – the key material is derived from the device’s credential storage. Both mechanisms are audited and supported by the OS, so you don’t need to manage encryption keys yourself.
Limits and practical constraints
| Limit | Details |
|---|---|
| Maximum entry size | Typically 4–10 KB depending on the OS; larger strings trigger QuotaExceededError on Android. |
| Data type | Only strings are accepted. Convert objects to JSON or base64 before storing. |
| Binary blobs | Not supported – encode to base64 if you must store binary data. |
| Persistence across device reset | Data is lost if the user clears the Keychain or performs a factory reset. |
| Key rotation | SecureStore does not provide key rotation; rotate secrets at the application level. |
Common mistakes and how to avoid them
- Forgetting to link in bare workflow: If you skip
npx pod-installon iOS or rebuild on Android,SecureStore.setItemAsyncwill throw a runtime error. Verify by runningnpx react-native run-androidafter installation. - Storing large JSON objects: A 15 KB JSON string will fail on Android. Split the object into smaller keys or compress it before encoding.
- Storing private keys or highly sensitive secrets: Even though the data is encrypted, the module does not provide additional cryptographic protection. For such data, use a dedicated HSM or server‑side key‑management system.
- Assuming data survives device wipe: Users can clear the Keychain or reset the device, wiping all SecureStore entries. Write fallback logic that can re‑fetch or regenerate secrets if
getItemAsyncreturnsnull. - Not handling
QuotaExceededError: WrapsetItemAsyncin a try/catch and handle the error gracefully. Example:
try {
await SecureStore.setItemAsync('bigKey', largeString);
} catch (e) {
if (e.code === 'QuotaExceededError') {
console.warn('Data too large for SecureStore');
}
}
Verifying the behavior yourself
- Create a new Expo project:
expo init SecureDemo→ choose the blank template. - Install the module:
expo install expo-secure-store. - Paste the example snippet into
App.jsand runexpo start. - Open the console in the simulator/emulator or use
adb logcatfor Android to see the output. - Close the app, reopen it, and confirm the console still prints the stored value.
- To test the size limit, replace the string with
'a'.repeat(12000)and observe aQuotaExceededErroron Android.
When to use SecureStore versus other storage options
- AsyncStorage: Good for non‑sensitive data; no encryption.
- Keychain/EncryptedSharedPreferences directly: Requires native code and is not supported in the managed workflow.
- Third‑party libraries (e.g., react-native-keychain): Useful in bare projects;
expo-secure-storeprovides the same functionality with a simpler import path. - For large data sets or structured data, consider a local database (SQLite) with optional encryption.
Conclusion
Expo SecureStore offers a lightweight, cross‑platform way to keep small secrets safe. By using the provided API, developers avoid the pitfalls of manual encryption and native integration. Just remember the size and data‑type limits, handle potential errors, and rotate secrets at the application level to maintain security over time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.