storage — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited storage (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Two options. Pick based on sensitivity. Never AsyncStorage.
| Data | Storage |
|---|---|
| Auth tokens, passwords, keys | expo-secure-store |
| User preferences, settings, cache | MMKV |
| Draft content, offline queue | MMKV |
| Onboarding state, feature flags | MMKV |
| Biometric keys, encryption keys | expo-secure-store |
Never `AsyncStorage` -- synchronous MMKV is always faster, SecureStore is always safer for sensitive data.
bun add react-native-mmkvsrc/libs/storage.ts:
import { MMKV } from 'react-native-mmkv'
export const storage = new MMKV({ id: 'app-storage' })
// Typed helpers
export const appStorage = {
// Preferences
getTheme: () => storage.getString('theme') as 'light' | 'dark' | 'auto' | undefined,
setTheme: (theme: 'light' | 'dark' | 'auto') => storage.set('theme', theme),
// Onboarding
getOnboardingDone: () => storage.getBoolean('onboarding_done') ?? false,
setOnboardingDone: () => storage.set('onboarding_done', true),
// Cache timestamps
getLastSync: () => storage.getNumber('last_sync'),
setLastSync: (ts: number) => storage.set('last_sync', ts),
// Clear
clear: () => storage.clearAll(),
}import { createStore } from 'zustand-x'
import { storage } from '@/libs/storage'
type SettingsState = { theme: 'light' | 'dark' | 'auto'; language: string }
export const settingsStore = createStore<SettingsState>(
{
theme: storage.getString('theme') as 'light' | 'dark' | 'auto' ?? 'auto',
language: storage.getString('language') ?? 'en',
},
{ name: 'settings', mutative: true },
).extendActions((set, get) => ({
setTheme: (theme: SettingsState['theme']) => {
set.theme(theme)
storage.set('theme', theme) // persist immediately
},
setLanguage: (language: string) => {
set.language(language)
storage.set('language', language)
},
}))// src/libs/token.ts
import * as SecureStore from 'expo-secure-store'
export const tokenStorage = {
getAccess: () => SecureStore.getItemAsync('access_token'),
setAccess: (token: string) => SecureStore.setItemAsync('access_token', token),
getRefresh: () => SecureStore.getItemAsync('refresh_token'),
setRefresh: (token: string) => SecureStore.setItemAsync('refresh_token', token),
clear: () => Promise.all([
SecureStore.deleteItemAsync('access_token'),
SecureStore.deleteItemAsync('refresh_token'),
]),
}SecureStore is async (reads from Keychain/Keystore). For app startup:
// Preload token at app start to avoid waterfall
const token = await SecureStore.getItemAsync('access_token')type QueuedAction = { id: string; type: string; payload: unknown; timestamp: number }
const QUEUE_KEY = 'offline_queue'
export const offlineQueue = {
get: (): QueuedAction[] => {
const raw = storage.getString(QUEUE_KEY)
return raw ? JSON.parse(raw) : []
},
add: (action: Omit<QueuedAction, 'id' | 'timestamp'>) => {
const queue = offlineQueue.get()
queue.push({ ...action, id: crypto.randomUUID(), timestamp: Date.now() })
storage.set(QUEUE_KEY, JSON.stringify(queue))
},
remove: (id: string) => {
const queue = offlineQueue.get().filter(a => a.id !== id)
storage.set(QUEUE_KEY, JSON.stringify(queue))
},
clear: () => storage.delete(QUEUE_KEY),
}SecureStore for anything sensitive -- tokens, keys, passwordsMMKV for preferences, cache, state that survives app restartAsyncStorage -- deprecated by community, synchronous MMKV is betterdeleteItemAsync on sign out to clear SecureStore~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.