◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-21Advanced

Where Should Your Rork App Store Auth Tokens? expo-secure-store and a Biometric Gate

How I moved a Rork-generated app's auth tokens out of AsyncStorage into expo-secure-store and put a biometric check in front of every read — including the size limit and sign-out gotchas I hit in production.

Rork611expo-secure-store3authentication5security5biometrics

✦ Premium Article

When you run a handful of apps for long enough as an indie developer, you eventually notice that the riskiest line of code is usually the first one you ever wrote — and never touched again. For me it was the auth token. The login flow Rork generated wrote both the access and refresh tokens straight into AsyncStorage. It worked, so it sat there for six months.

AsyncStorage is not encrypted. On Android the values live in a plain SQLite store; on iOS they get some protection but not the strength you'd consciously choose for a long-lived secret. If you assume a rooted or jailbroken device, or a leaky backup, plaintext tokens are simply not where credentials belong.

This article walks through moving tokens into expo-secure-store and then putting a biometric check in front of the read — migration and cleanup included. Token refresh and retry are a separate topic; here I'm focused only on storage and retrieval.

You don't have one storage — split by role

The first trap is thinking "just put everything in SecureStore." SecureStore goes through the OS keychain/keystore, so every read and write carries real cost. Push your settings and cache through it and your cold start gets visibly slower.

Split by the nature of the data instead.

DataStoreWhy
Access/refresh tokens, user secretsexpo-secure-storeA leak is immediate harm. Protected by the OS keychain/keystore
Theme, language, onboarding-complete flagsAsyncStorageLow harm if exposed. Cheap to read and write
API response cache, image metadata, bulk dataMMKV / SQLiteHigh volume, needs fast access. Low secrecy

Isolating only the tokens in SecureStore narrows the attack surface considerably while keeping startup overhead minimal.

The basics, and the cost of a read

If you're inside the managed Expo flow, install is one line.

npx expo install expo-secure-store

A thin wrapper pays off later when you start adding options. Keep keys as constants so a typo can't silently split your storage.

// lib/secureToken.ts
import * as SecureStore from "expo-secure-store";
 
const ACCESS_KEY = "auth.accessToken";
const REFRESH_KEY = "auth.refreshToken";
 
// iOS: decryptable only after unlock, never migrates to another device
// Android: AES-encrypted and held in the Keystore
const OPTIONS: SecureStore.SecureStoreOptions = {
  keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
};
 
export async function saveTokens(access: string, refresh: string) {
  await SecureStore.setItemAsync(ACCESS_KEY, access, OPTIONS);
  await SecureStore.setItemAsync(REFRESH_KEY, refresh, OPTIONS);
}
 
export async function getAccessToken(): Promise<string | null> {
  return SecureStore.getItemAsync(ACCESS_KEY, OPTIONS);
}
 
export async function clearTokens() {
  await SecureStore.deleteItemAsync(ACCESS_KEY);
  await SecureStore.deleteItemAsync(REFRESH_KEY);
}

The default for keychainAccessible is WHEN_UNLOCKED, but I usually reach for the *_THIS_DEVICE_ONLY variants. It's a deliberate statement that the token must not travel to another device through iCloud Keychain or a backup. If you genuinely need to refresh tokens in the background, loosen it to AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY — but decide that from the requirement, not by default.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A decision rule for splitting tokens, settings, and cache across SecureStore, AsyncStorage, and MMKV
✦Putting biometric auth in front of the token read, with a fallback path for lockouts
✦A run-once migration off AsyncStorage and a sign-out routine that actually clears the device
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-27
Claude Sonnet 4.5 Leaves the API on November 30. Find the Model IDs Hard-Coded in Your App First
Claude Sonnet 4.5 is scheduled to retire from the API on November 30, 2026. A small script finds the model IDs written into your app's code and config, so you can replace them and check again.
⬡ Dev Tools2026-10-27
Your EAS Update Opens in Expo Go on iPhone but Stops on Android with "Failed to download remote update". The 403 You Get Even When Signed In
An EAS Update that opens in Expo Go on iOS but stops on Android (SDK 57) with Failed to download remote update. The cause is a sign-in token saved in one place and read from another, and the fix sits on the SDK 58 side. The order to check things in, and what to run yourself.