Week 9: On-Device Storage: AsyncStorage, SecureStore & SQLite

Every app in this course so far has assumed data either lives in memory or arrives from a network call. This week closes that gap with three ways to store data directly on the device — a plain key-value store, an encrypted store for secrets, and a real embedded SQL database — and a clear rule for which one fits which job.

Phase 4 of 8 Week 9 of 22 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Store and retrieve simple key-value data with AsyncStorage, and know its limits
  • Store sensitive values like auth tokens with SecureStore instead
  • Model structured, queryable local data with expo-sqlite
  • Pick the right storage option for a given kind of data

1. AsyncStorage Basics & Limits

terminal
npx expo install @react-native-async-storage/async-storage
reading and writing a simple value
import AsyncStorage from '@react-native-async-storage/async-storage';

async function saveDraft(text: string) {
  await AsyncStorage.setItem('draft-note', text);
}

async function loadDraft() {
  return await AsyncStorage.getItem('draft-note'); // string | null
}

async function saveObject(key: string, value: object) {
  await AsyncStorage.setItem(key, JSON.stringify(value));
}

AsyncStorage is a flat, unencrypted, string-only key-value store — everything written to it, including anything you JSON.stringify into it, is readable in plain text by anyone with access to the device's file system. It's the right tool for drafts, cached lists, and UI preferences; it is not the right tool for anything sensitive.

2. SecureStore for Sensitive Data

expo-secure-store uses the platform's real encrypted storage — the iOS Keychain and Android's Keystore-backed EncryptedSharedPreferences — for anything that actually needs protecting.

terminal
npx expo install expo-secure-store
storing an auth token
import * as SecureStore from 'expo-secure-store';

async function saveToken(token: string) {
  await SecureStore.setItemAsync('auth-token', token);
}

async function getToken() {
  return await SecureStore.getItemAsync('auth-token');
}

async function clearToken() {
  await SecureStore.deleteItemAsync('auth-token');
}

SecureStore has a much smaller per-value size limit than AsyncStorage (around 2KB on iOS) — it's built for tokens and credentials, not for caching a JSON response.

3. Structured Data with expo-sqlite

Once data has real structure and needs to be queried — a list of notes with tags and dates, not just one blob — a real embedded SQL database beats hand-rolled JSON parsing on every read.

terminal
npx expo install expo-sqlite
db/notes.ts
import * as SQLite from 'expo-sqlite';

const db = SQLite.openDatabaseSync('notes.db');

export function initDb() {
  db.execSync(
    'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL, body TEXT, created_at INTEGER);'
  );
}

export function addNote(title: string, body: string) {
  db.runSync(
    'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?);',
    [title, body, Date.now()]
  );
}

export function getAllNotes() {
  return db.getAllSync('SELECT * FROM notes ORDER BY created_at DESC;');
}

The synchronous *Sync methods shown here run on the JS thread and are fine for typical note-taking-app volumes; for large datasets or heavier queries, expo-sqlite also exposes async equivalents that don't block rendering.

4. Choosing the Right Storage

Kind of dataUse
UI preferences, drafts, cached API responsesAsyncStorage
Auth tokens, API keys, anything secretSecureStore
Structured, queryable records (notes, transactions, offline-first data)expo-sqlite

These aren't mutually exclusive — a real app typically uses all three at once: a refresh token in SecureStore, a "last opened tab" preference in AsyncStorage, and the user's actual content in SQLite.

5. Hands-on Exercise

Hands-on

Build an offline notes app with a secure "remember me"

Combine all three storage types in one small app, each used for what it's actually good at.

Requirements:

  1. A notes screen backed by expo-sqlite: create, list (newest first), and delete notes, each with a title and body.
  2. A "remember me" toggle on a fake sign-in screen that, when on, stores a mock session token in SecureStore and restores the signed-in state on relaunch.
  3. A UI preference (e.g. sort order, or dark/light override) stored in AsyncStorage and restored on launch.
  4. Confirm behavior on force-quit and relaunch: notes persist, remember-me restores the session (or doesn't, if it wasn't checked), and the preference is remembered.
  5. A short comment in the code next to each storage call explaining why that storage type was chosen for that data.
Hint

If db.getAllSync throws about a missing table, confirm initDb() actually ran before any query — it's easy to call it after the first screen has already tried to read, especially if it's tucked inside a useEffect that fires later than a sibling component's own data-loading effect.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why is AsyncStorage the wrong place to store an auth token, even though it can technically hold a string of any content?

AsyncStorage is unencrypted plain-text storage on disk — anything written to it, including a token, is readable by anyone with file-system access to the device (more of a risk than it sounds, especially on rooted/jailbroken devices). SecureStore backs onto the platform's real encrypted storage (Keychain/Keystore), which is what tokens and credentials are meant to use.

Q2

Why would you reach for expo-sqlite instead of just storing a JSON array of notes in AsyncStorage?

A JSON blob in AsyncStorage has to be parsed and re-serialized in full on every read and write, and any filtering or sorting happens in JS after loading everything into memory. SQLite lets the database itself query, filter, and sort — ORDER BY, WHERE — which scales far better once there's more than a handful of records.

Q3

What's the practical size limit that pushes you away from SecureStore for larger values?

SecureStore is built for small secrets like tokens, with a per-value size limit around 2KB on iOS — trying to cache a full JSON API response or a list of records in it will fail or behave unreliably. AsyncStorage (for non-sensitive data) or SQLite (for structured data) have no such tight ceiling.

Q4

In the notes app exercise, why store the "remember me" token separately from the notes themselves?

The token is a credential — exactly the kind of sensitive value SecureStore exists for — while the notes are the user's actual structured content, which belongs in a real queryable store. Mixing them into one storage mechanism would either under-protect the token or over-engineer storage for data that doesn't need encryption.