From harness-claude
Persist mobile app data using AsyncStorage for preferences, SecureStore for tokens, MMKV for fast key-value, and SQLite for relational data.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Persist data on mobile with AsyncStorage, SecureStore, MMKV, and SQLite for different use cases
Guides data fetching in React Native/Expo apps using Fetch API and TanStack Query, covering API calls, caching, mutations, authentication tokens, offline support, and request cancellation.
Routes iOS data tasks to specialized skills for SwiftData, Core Data, GRDB, SQLite, CloudKit sync, Codable serialization, migrations, Keychain, and storage strategies.
Implements offline-first mobile apps with local storage (AsyncStorage, Realm, SQLite), sync strategies, background sync, and conflict resolution.
Share bugs, ideas, or general feedback.
Persist data on mobile with AsyncStorage, SecureStore, MMKV, and SQLite for different use cases
| 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) |
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'],
]);
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
});
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',
});
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),
}
)
);
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]);
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() }));
}
}
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
}
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:
https://docs.expo.dev/versions/latest/sdk/async-storage/