skip to content

Expo SecureStore

Expo SecureStore keeps small secrets encrypted in the iOS Keychain and Android Keystore, optionally behind biometrics. Interviewers ask where tokens live on a device and what survives an uninstall.

on this pageshow

explore

questions

6

In an Expo app, how do you save, read and delete a session token with expo-secure-store, and where does the value actually live on iOS and Android?

level: juniorimportance: must knowfreq 55%

answer

  1. three async calls, one key
  2. missing key resolves null
  3. strings only; JSON-encode the rest
  4. Keychain item on iOS
  5. Keystore-encrypted prefs on Android

basics

~20 s

Call SecureStore.setItemAsync(key, value) to save, getItemAsync(key) to read (it resolves null when nothing is stored) and deleteItemAsync(key) on sign-out. iOS keeps the value as a Keychain item; Android encrypts it with a Keystore key into SharedPreferences.

solid answer

~40 s

`import * as SecureStore from 'expo-secure-store'`, then `await SecureStore.setItemAsync('session_token', token)` after sign-in, `await SecureStore.getItemAsync('session_token')` at launch, which resolves `null` when no entry exists, and `await SecureStore.deleteItemAsync('session_token')` on sign-out. Values must be strings, so JSON-encode anything else, and keys may only contain letters, digits, `.`, `-` and `_`; anything else throws. On iOS the value is a generic-password Keychain item; on Android it is encrypted with an AES key held in the Android Keystore and the ciphertext is saved in a `SecureStore` SharedPreferences file. There are synchronous `getItem` and `setItem` too, but they block the JavaScript thread. The module is not available on web: `isAvailableAsync()` resolves `true` only on Android and iOS.

code

typescript · 15 lines
typescript
import * as SecureStore from 'expo-secure-store';

const TOKEN_KEY = 'session_token';

export async function saveSession(token: string): Promise<void> {
  await SecureStore.setItemAsync(TOKEN_KEY, token);
}

export async function loadSession(): Promise<string | null> {
  return SecureStore.getItemAsync(TOKEN_KEY); // null when signed out
}

export async function clearSession(): Promise<void> {
  await SecureStore.deleteItemAsync(TOKEN_KEY);
}

go deeper

for a junior

Recall the three calls, setItemAsync, getItemAsync and deleteItemAsync, that values are strings, and that a missing key resolves null.

for a middle

Explain the storage on each platform, Keychain item on iOS and Keystore-encrypted SharedPreferences on Android, and the key and value validation rules.

for a senior

Treat SecureStore as a secure cache: keep secrets small, avoid sync calls in the UI path, plan the web build, and never rely on it as the only copy.

for a principal

Decide which credentials the device should hold at all, and for how long, given what the server can reissue and what a stolen device exposes.

## What expo-secure-store is for `expo-secure-store` stores **small secrets** as string key-value pairs, encrypted by the operating system's own credential facilities. In a crypto-price tracker, that is the session token the app sends with every price-alert request, or a refresh token. It is not a general database and not a cache; it is where a short, sensitive string goes. Each Expo project has its own separate storage, with no access to other projects' entries. ## The API in SDK 57 | Call | Returns | Notes | |---|---|---| | `setItemAsync(key, value, options?)` | `Promise<void>` | rejects if the value cannot be stored | | `getItemAsync(key, options?)` | `Promise<string \| null>` | `null` when there is no entry | | `deleteItemAsync(key, options?)` | `Promise<void>` | rejects if the value cannot be deleted | | `getItem(key, options?)` / `setItem(key, value, options?)` | value / `void` | synchronous, block the JS thread | | `isAvailableAsync()` | `Promise<boolean>` | `true` on Android and iOS only | | `canUseBiometricAuthentication()` | `boolean` | for the `requireAuthentication` option | Rules the library enforces in JavaScript before anything reaches native code: - **Keys** must be non-empty and contain only alphanumeric characters, `.`, `-` and `_`. A key such as `session token` or `user@example` throws. - **Values** must be strings. Passing an object or number throws an error suggesting you JSON-encode it. There is no synchronous delete; `deleteItemAsync` is the only removal call. ## Where the bytes live **iOS.** Each entry is a Keychain item of class `kSecClassGenericPassword`. The item's service attribute is derived from the `keychainService` option (with a default when you pass none), and its account is the key. The Keychain is managed by the operating system, encrypted by it, and governed by an accessibility attribute you can choose with `keychainAccessible` (default `WHEN_UNLOCKED`). **Android.** The module generates a 256-bit AES key inside the **Android Keystore**, where the key material cannot be exported, encrypts the value with AES-GCM, and writes the ciphertext as JSON into a private SharedPreferences file named `SecureStore`. Without the Keystore key, which only this app on this device can use, the file's contents are unreadable. ## The usual token flow 1. After sign-in, `setItemAsync('session_token', token)`. 2. At launch, `getItemAsync('session_token')`; `null` means show the sign-in screen. 3. On sign-out, or when the server rejects the token, `deleteItemAsync('session_token')`. Keep the token in memory once read, rather than reading it from SecureStore on every request. Each read crosses into native code and the platform's credential store. ## What it does not do - **It does not work on web.** The web implementation is empty, which is why `isAvailableAsync()` exists. A universal app needs another strategy for its web build. - **It is not meant for large data.** Expo does not enforce a size limit, but the platform can reject large payloads, and some iOS releases historically refused values above roughly 2 KB. - **It does not guarantee lifetime.** On iOS an entry can outlive an uninstall; on Android it does not survive one. Treat it as a secure cache of credentials the server can reissue, never the only copy of something irreplaceable. - **It does not make the rest of the app secure.** A token copied into a global store, a log line or plain storage is exposed there, whatever SecureStore does. ## Options must match on read The same `options` object shapes where an entry lives, so reads must use what writes used: - An item saved with a `keychainService` must be read and deleted with that same `keychainService`; the docs say it is required to fetch the value later. - On iOS, `accessGroup` likewise selects which shared Keychain group the item belongs to. - `keychainAccessible` only matters when the item is written. A helper module that owns the keys and their options, like the one in the example, prevents a screen from reading `session_token` with different options and getting `null` for a value that is actually there. ## Synchronous calls `getItem` and `setItem` exist for startup code that cannot await, but they block the JavaScript thread for the duration of the native call. With `requireAuthentication`, they block until the user finishes the biometric prompt, freezing the UI, so the async versions are the default choice.

  • Why does SecureStore.setItemAsync('user token', t) throw before touching the Keychain?
    expo-secure-store validates keys in JavaScript: they must be non-empty and contain only alphanumeric characters, `.`, `-` and `_`. The space fails that check, so it throws an invalid-key error. Use `user_token` or `user.token` instead.
  • How should a universal Expo app handle the web build if it uses expo-secure-store?
    The web implementation is empty, so check `isAvailableAsync()` or branch on the platform and use a web-appropriate mechanism there, typically keeping the session in an HttpOnly cookie managed by the server rather than storing the token in JavaScript.

saying these in an interview costs you the question

  • getItemAsync throws when no value is stored for the key
  • SecureStore accepts objects and serialises them for you
  • On Android, SecureStore values are stored in plain SharedPreferences
  • expo-secure-store works the same on web using localStorage
  • Any string, including spaces or @, is a valid SecureStore key
open as a page

A user deletes and reinstalls an Expo app and is still signed in on iOS but not on Android; how does expo-secure-store explain that?

level: seniorimportance: must knowfreq 35%

basics

~20 s

On iOS, expo-secure-store values live in the Keychain, which can survive uninstalling when the app is reinstalled with the same bundle ID. On Android the Keystore keys are deleted with the app, so values are gone, and Auto Backup is configured to exclude them.

open as a page

In expo-secure-store, what does the iOS keychainAccessible option control, and which value suits a token read by background refresh?

level: middleimportance: should knowfreq 28%

basics

~20 s

keychainAccessible sets the iOS Keychain accessibility of the stored item, meaning when it can be read. The default WHEN_UNLOCKED fails while the device is locked, so a token read by background refresh needs AFTER_FIRST_UNLOCK or its THIS_DEVICE_ONLY variant.

open as a page

In expo-secure-store, what does requireAuthentication: true change, and how does it behave differently on iOS and Android?

level: middleimportance: should knowfreq 30%

basics

~20 s

requireAuthentication: true ties the stored value to the user's biometrics, so the system prompts before releasing it. On iOS the prompt appears when reading or updating an existing value; on Android every operation requires authentication.

open as a page

After a user adds a new fingerprint, an Expo app's biometric-protected expo-secure-store value reads as null; why, and how should the app recover?

level: seniorimportance: should knowfreq 18%

basics

~20 s

Values saved with requireAuthentication: true are bound to the currently enrolled biometrics, so enrolling a new fingerprint invalidates them and getItemAsync resolves null. Treat that as expired: sign the user in another way, then save a fresh value.

open as a page

In an Expo app, why is expo-secure-store the wrong place for a large JSON payload such as a cached crypto portfolio, and what should hold it instead?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

expo-secure-store is meant for small secrets: Expo enforces no size limit, but the platform can reject large payloads, and some iOS releases refused values above about 2 KB. Keep the portfolio in a database or file, and store only its encryption key in SecureStore.

open as a page