git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/mobile-storage-patterns" ~/.claude/skills/intense-visions-harness-engineering-mobile-storage-patterns-557174 && rm -rf "$T"
agents/skills/claude-code/mobile-storage-patterns/SKILL.mdMobile Storage Patterns
Persist data on mobile with AsyncStorage, SecureStore, MMKV, and SQLite for different use cases
When to Use
- Storing user preferences, settings, or onboarding state
- Caching API responses for offline access
- Persisting authentication tokens securely
- Storing structured relational data locally
- Choosing the right storage solution for your data type
Instructions
- Choose the right storage for your data type:
| Solution | Best For | Capacity | Speed | Security |
|---|---|---|---|---|
| AsyncStorage | Simple key-value (settings, flags) | ~6MB | Moderate | None |
| expo-secure-store | Tokens, passwords, API keys | ~2KB per item | Moderate | Keychain/Keystore |
| MMKV | High-frequency reads/writes, state persistence | ~unlimited | Very fast | Optional encryption |
| SQLite | Structured relational data, complex queries | ~unlimited | Fast | None (file-level) |
- Use AsyncStorage for simple preferences and flags.
import AsyncStorage from '@react-native-async-storage/async-storage'; // Store await AsyncStorage.setItem('onboarding_complete', 'true'); await AsyncStorage.setItem('user_preferences', JSON.stringify({ theme: 'dark', locale: 'en' })); // Retrieve const isComplete = await AsyncStorage.getItem('onboarding_complete'); const prefs = JSON.parse((await AsyncStorage.getItem('user_preferences')) ?? '{}'); // Remove await AsyncStorage.removeItem('onboarding_complete'); // Multi operations await AsyncStorage.multiSet([ ['key1', 'value1'], ['key2', 'value2'], ]);
- Use SecureStore for sensitive credentials. Values are encrypted using iOS Keychain or Android Keystore.
import * as SecureStore from 'expo-secure-store'; // Store securely await SecureStore.setItemAsync('auth_token', token); await SecureStore.setItemAsync('refresh_token', refreshToken); // Retrieve const token = await SecureStore.getItemAsync('auth_token'); // Delete await SecureStore.deleteItemAsync('auth_token'); // With options await SecureStore.setItemAsync('biometric_key', value, { keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, requireAuthentication: true, // Requires biometric to read });
- Use MMKV for high-performance key-value storage. MMKV is ~30x faster than AsyncStorage and supports synchronous access.
npx expo install react-native-mmkv
import { MMKV } from 'react-native-mmkv'; const storage = new MMKV(); // Synchronous — no await needed storage.set('user.id', '12345'); storage.set('user.premium', true); storage.set('last_sync', Date.now()); const userId = storage.getString('user.id'); const isPremium = storage.getBoolean('user.premium'); storage.delete('user.id'); // With encryption const secureStorage = new MMKV({ id: 'secure-storage', encryptionKey: 'your-encryption-key', });
- Integrate MMKV with Zustand for persisted state management.
import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { MMKV } from 'react-native-mmkv'; const storage = new MMKV(); const mmkvStorage = { getItem: (name: string) => storage.getString(name) ?? null, setItem: (name: string, value: string) => storage.set(name, value), removeItem: (name: string) => storage.delete(name), }; const useSettingsStore = create( persist( (set) => ({ theme: 'light' as 'light' | 'dark', setTheme: (theme: 'light' | 'dark') => set({ theme }), }), { name: 'settings-storage', storage: createJSONStorage(() => mmkvStorage), } ) );
- Use SQLite for structured relational data.
npx expo install expo-sqlite
import * as SQLite from 'expo-sqlite'; const db = await SQLite.openDatabaseAsync('app.db'); // Create tables await db.execAsync(` CREATE TABLE IF NOT EXISTS orders ( id TEXT PRIMARY KEY, customer_name TEXT NOT NULL, total REAL NOT NULL, status TEXT DEFAULT 'pending', created_at TEXT DEFAULT CURRENT_TIMESTAMP ); `); // Insert await db.runAsync('INSERT INTO orders (id, customer_name, total) VALUES (?, ?, ?)', [ orderId, customerName, total, ]); // Query const orders = await db.getAllAsync<Order>( 'SELECT * FROM orders WHERE status = ? ORDER BY created_at DESC LIMIT ?', ['pending', 20] ); // Single row const order = await db.getFirstAsync<Order>('SELECT * FROM orders WHERE id = ?', [orderId]);
- Implement a cache layer for API responses.
class ApiCache { private storage = new MMKV({ id: 'api-cache' }); async get<T>(key: string, maxAge: number): Promise<T | null> { const cached = this.storage.getString(key); if (!cached) return null; const { data, timestamp } = JSON.parse(cached); if (Date.now() - timestamp > maxAge) { this.storage.delete(key); return null; } return data as T; } set<T>(key: string, data: T): void { this.storage.set(key, JSON.stringify({ data, timestamp: Date.now() })); } }
- Clean up storage on logout. Clear user-specific data but retain app-level preferences.
async function clearUserData() { await SecureStore.deleteItemAsync('auth_token'); await SecureStore.deleteItemAsync('refresh_token'); storage.delete('user.id'); storage.delete('user.premium'); // Keep: theme, locale, onboarding_complete }
Details
AsyncStorage limitations: AsyncStorage is asynchronous, unencrypted, and has platform-specific size limits (~6MB on Android by default). It serializes to JSON, so large datasets are slow. Use it only for small, non-sensitive data.
SecureStore limitations: Individual values are limited to ~2KB. It is async and not suitable for high-frequency reads. Use only for authentication tokens, API keys, and sensitive credentials.
MMKV advantages: Memory-mapped I/O, synchronous API, ~30x faster than AsyncStorage, supports encryption, and has no practical size limit. It is the recommended replacement for AsyncStorage in performance-sensitive apps.
SQLite considerations: Use for data with relationships (users, orders, products), offline-first apps that need complex queries, or datasets too large for key-value storage. Consider using a migration library for schema changes.
Common mistakes:
- Storing auth tokens in AsyncStorage (unencrypted, accessible to other apps on rooted devices)
- Storing large JSON blobs in key-value storage (use SQLite for structured data)
- Not handling storage errors (device may be full)
- Synchronous MMKV reads blocking the UI thread with very large values
Source
https://docs.expo.dev/versions/latest/sdk/async-storage/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.