BuildBot

React, Off the Web

Auth and persistence

Lesson 4 of 5

What you'll learn

  • Choose between expo-secure-store and AsyncStorage for each kind of data
  • Implement the standard session pattern: restore token on launch, verify, refresh
  • Understand why native OAuth goes through a hosted browser flow, not hand-rolled tokens

Web auth leans on the browser: httpOnly cookies ride along on every request, and the server owns the session. A native app has no cookie jar you can trust and no origin — you hold the token yourself and decide where it lives. Expo gives you two very different boxes:

| | expo-secure-store | @react-native-async-storage/async-storage | | --- | --- | --- | | Backed by | iOS Keychain / Android Keystore | Unencrypted on-device storage | | Encrypted | Yes | No | | Size | Small values (~2 KB) | Larger blobs, fine for JSON | | Use for | Tokens, keys, anything secret | Cached data, preferences, drafts |

The session pattern almost every app converges on:

import * as SecureStore from "expo-secure-store";

const KEY = "session_token";

export async function saveSession(token: string) {
  await SecureStore.setItemAsync(KEY, token);
}

// On launch: restore, verify with the server, refresh or clear.
export async function restoreSession(): Promise<string | null> {
  const token = await SecureStore.getItemAsync(KEY);
  if (!token) return null;
  const res = await fetch("https://api.example.com/me", {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) {
    await SecureStore.deleteItemAsync(KEY);
    return null;
  }
  return token;
}

Your root layout awaits restoreSession() behind the splash screen, then redirects into (auth) or (tabs) — the route groups from lesson 2 earning their keep.

OAuth: use the browser, on purpose

For "Sign in with Google/Apple/your IdP", resist the urge to collect credentials in your own UI or to hand-roll a native token exchange. The robust pattern is a hosted, browser-based SSO flow: open your provider's web login in a secure in-app browser session (expo-web-browser / expo-auth-session), let the user authenticate there, and receive the callback on a redirect URI (deep link) that lands back in your app. You get the provider's real login page, existing web sessions, 2FA, and passkeys for free — and one identity pipeline shared with your web app. Hand-rolled native token exchanges routinely desync from the hosted flow (different user identifiers, different claims), and merging those accounts later is painful.

Offline-first state

Mobile networks vanish in elevators. The baseline pattern: keep server data in memory (your query library of choice), mirror the last-known snapshot into AsyncStorage, and rehydrate it on launch so the app opens showing real content — stale, labeled, but present — while a background refetch reconciles. Writes made offline go into a queue that drains when connectivity returns.

Secrets in the right box

The refresh token belongs in SecureStore; the cached profile JSON belongs in AsyncStorage. Never the reverse: AsyncStorage is readable on a compromised device, and SecureStore is too small for data blobs.

The challenge models launch-time session restore: a "device" whose storage outlives each app process.

Session restore across restarts (JS model)

Sign in, then hit 'Restart app' — state resets but SecureStore survives, so restoreSession() finds the token. Sign out and restart to see the logged-out path.

Loading editor…
Knowledge check

Where should a session refresh token live in an Expo app?

Next: getting all of this onto real phones — EAS Build, store submission, and over-the-air updates.

Saved on this device. Sign in to sync your progress everywhere.