react-native-nitro-storage 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +237 -862
  2. package/SECURITY.md +26 -0
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
  4. package/docs/api-reference.md +217 -0
  5. package/docs/batch-transactions-migrations.md +186 -0
  6. package/docs/benchmarks.md +37 -0
  7. package/docs/mmkv-migration.md +80 -0
  8. package/docs/react-hooks.md +113 -0
  9. package/docs/recipes.md +281 -0
  10. package/docs/secure-storage.md +171 -0
  11. package/docs/web-backends.md +141 -0
  12. package/ios/IOSStorageAdapterCpp.mm +44 -14
  13. package/lib/commonjs/index.js +271 -5
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +498 -202
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/indexeddb-backend.js +129 -7
  18. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  19. package/lib/commonjs/storage-runtime.js +41 -0
  20. package/lib/commonjs/storage-runtime.js.map +1 -0
  21. package/lib/commonjs/web-storage-backend.js +90 -0
  22. package/lib/commonjs/web-storage-backend.js.map +1 -0
  23. package/lib/module/index.js +263 -5
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/index.web.js +490 -202
  26. package/lib/module/index.web.js.map +1 -1
  27. package/lib/module/indexeddb-backend.js +129 -7
  28. package/lib/module/indexeddb-backend.js.map +1 -1
  29. package/lib/module/storage-runtime.js +36 -0
  30. package/lib/module/storage-runtime.js.map +1 -0
  31. package/lib/module/web-storage-backend.js +86 -0
  32. package/lib/module/web-storage-backend.js.map +1 -0
  33. package/lib/typescript/index.d.ts +14 -7
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/index.web.d.ts +15 -8
  36. package/lib/typescript/index.web.d.ts.map +1 -1
  37. package/lib/typescript/indexeddb-backend.d.ts +6 -2
  38. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  39. package/lib/typescript/storage-runtime.d.ts +48 -0
  40. package/lib/typescript/storage-runtime.d.ts.map +1 -0
  41. package/lib/typescript/web-storage-backend.d.ts +30 -0
  42. package/lib/typescript/web-storage-backend.d.ts.map +1 -0
  43. package/package.json +21 -8
  44. package/src/index.ts +330 -20
  45. package/src/index.web.ts +673 -245
  46. package/src/indexeddb-backend.ts +147 -6
  47. package/src/storage-runtime.ts +129 -0
  48. package/src/web-storage-backend.ts +129 -0
package/README.md CHANGED
@@ -1,84 +1,79 @@
1
1
  # react-native-nitro-storage
2
2
 
3
- The fastest, most complete storage solution for React Native.
4
- Synchronous Memory, Disk, and Secure storage in one unified API — powered by [Nitro Modules](https://github.com/mrousavy/nitro) and JSI.
5
-
6
- ## Highlights
7
-
8
- - **Three storage scopes** in-memory, persistent disk, and hardware-encrypted secure storage
9
- - **Synchronous reads & writes** — no `async/await`, no bridge, zero serialization overhead for primitives
10
- - **React hooks** `useStorage`, `useStorageSelector`, `useSetStorage` with automatic re-renders
11
- - **Type-safe** — full TypeScript generics, custom serializers, schema validation with fallback
12
- - **Namespaces** isolate keys by feature, user, or tenant with automatic prefixing
13
- - **TTL expiration** — time-based auto-expiry with optional `onExpired` callback
14
- - **Biometric storage** — hardware-backed biometric protection on iOS & Android
15
- - **Auth storage factory** — `createSecureAuthStorage` for multi-token auth flows
16
- - **Batch operations** — atomic multi-key get/set/remove via native batch APIs
17
- - **Prefix queries** — fast key/value scans with `storage.getKeysByPrefix` and `storage.getByPrefix`
18
- - **Versioned writes** — optimistic concurrency with `item.getWithVersion()` and `item.setIfVersion(...)`
19
- - **Performance metrics** — observe operation timings and aggregate snapshots
20
- - **Web secure backend override** — plug custom secure storage backend on web
21
- - **IndexedDB backend** — drop-in `createIndexedDBBackend` factory for persistent web Secure storage with large payloads
22
- - **Bulk import** — load a raw `Record<string, string>` into any scope atomically with `storage.import`
23
- - **Transactions** — grouped writes with automatic rollback on error
24
- - **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
25
- - **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
26
- - **Cross-platform** iOS, Android, and web (`localStorage` fallback)
27
-
28
- ## Feature Coverage
29
-
30
- Every feature in this package is documented with at least one runnable example in this README:
31
-
32
- - Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
33
- - Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
34
- - Scopes (`Memory`, `Disk`, `Secure`) see Storage Scopes and multiple use cases
35
- - Namespaces — see Multi-Tenant / Namespaced Storage
36
- - TTL expiration + callbacks see OTP / Temporary Codes
37
- - Validation + recovery see Feature Flags with Validation
38
- - Biometric + access control see Biometric-protected Secrets
39
- - Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
40
- - Prefix utilities (`getKeysByPrefix`, `getByPrefix`) see Prefix Queries and Namespace Inspection
41
- - Versioned item API (`getWithVersion`, `setIfVersion`) — see Optimistic Versioned Writes
42
- - Metrics API (`setMetricsObserver`, `getMetricsSnapshot`, `resetMetrics`) — see Storage Metrics Instrumentation
43
- - Web secure backend override (`setWebSecureStorageBackend`, `getWebSecureStorageBackend`) — see Custom Web Secure Backend
44
- - IndexedDB backend factory (`createIndexedDBBackend`) see IndexedDB Backend for Web
45
- - Bulk import (`storage.import`) — see Bulk Data Import
46
- - Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
47
- - Transactions — see Transactions and Atomic Balance Transfer
48
- - Migrations (`registerMigration`, `migrateToLatest`) see Migrations
49
- - MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
50
- - Raw string API (`getString`, `setString`, `deleteString`) see Raw String API
51
- - Keychain locked detection (`isKeychainLockedError`) — see `isKeychainLockedError(err)`
52
- - Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
53
-
54
- ## Requirements
55
-
56
- | Dependency | Version |
57
- | ---------------------------- | ----------- |
58
- | `react-native` | `>= 0.75.0` |
59
- | `react-native-nitro-modules` | `>= 0.35.4` |
60
- | `react` | `>= 18.2.0` |
61
-
62
- ## Installation
63
-
64
- ```bash
3
+ [![npm](https://img.shields.io/npm/v/react-native-nitro-storage)](https://www.npmjs.com/package/react-native-nitro-storage)
4
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
6
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.4-black)](https://nitro.margelo.com/)
7
+
8
+ One storage layer for render-time state, persisted app state, and native secrets.
9
+
10
+ Nitro Storage is a synchronous React Native storage library built on Nitro Modules and JSI. It exposes typed Memory, Disk, and Secure storage with React hooks, batch APIs, transactions, migrations, biometric access control, MMKV migration helpers, and a web IndexedDB backend.
11
+
12
+ Use it when you want one storage API for React Native and web, with fast synchronous reads and native secure storage instead of mixing AsyncStorage, SecureStore, Keychain wrappers, MMKV adapters, and custom React state glue.
13
+
14
+ ## Contents
15
+
16
+ - [At a Glance](#at-a-glance)
17
+ - [Use It When](#use-it-when)
18
+ - [Why Nitro Storage](#why-nitro-storage)
19
+ - [Install](#install)
20
+ - [Quick Start](#quick-start)
21
+ - [Storage Scopes](#storage-scopes)
22
+ - [Docs](#docs)
23
+ - [Platform Support](#platform-support)
24
+ - [Security Model](#security-model)
25
+ - [Migration Paths](#migration-paths)
26
+ - [Choosing a Storage Library](#choosing-a-storage-library)
27
+ - [Production Checklist](#production-checklist)
28
+ - [Development](#development)
29
+
30
+ ## At a Glance
31
+
32
+ | Need | API or feature |
33
+ | --------------------------------- | ------------------------------------------------------------- |
34
+ | Read preferences during startup | `createStorageItem` with `StorageScope.Disk` |
35
+ | Keep session-only state | `StorageScope.Memory` |
36
+ | Store auth tokens or secrets | `StorageScope.Secure` |
37
+ | Protect a value with biometrics | `biometric: true` and `biometricLevel` |
38
+ | Bind storage to React | `useStorage`, `useStorageSelector`, `useSetStorage` |
39
+ | Bootstrap several values at once | `getBatch`, `setBatch`, `removeBatch` |
40
+ | Roll back local writes on failure | `runTransaction` |
41
+ | Upgrade local schemas | `registerMigration` and `migrateToLatest` |
42
+ | Move existing MMKV data | `migrateFromMMKV` |
43
+ | Persist storage on web | `setWebDiskStorageBackend` or `createIndexedDBBackend` |
44
+ | Inspect secure backend state | `getSecurityCapabilities`, `getSecureMetadata`, metadata APIs |
45
+
46
+ ## Use It When
47
+
48
+ Nitro Storage is a good fit when your app needs synchronous local reads and a typed API across preferences, auth state, feature flags, and secrets. It is especially useful for startup gates, theme or locale preferences, persisted onboarding state, refresh tokens, biometric-protected values, and React state that should survive reloads.
49
+
50
+ Use a database or server-state cache instead when you need relational queries, conflict resolution, large collections, sync protocols, pagination, or cache invalidation from remote APIs.
51
+
52
+ ## Why Nitro Storage
53
+
54
+ - Synchronous reads for startup state, render-time preferences, and auth boundaries.
55
+ - Typed `StorageItem<T>` values with custom serialization, validation, TTL, optimistic writes, and subscriptions.
56
+ - Three scopes: in-memory session state, persisted disk state, and platform secure storage.
57
+ - Secure storage backed by iOS Keychain and Android Keystore/EncryptedSharedPreferences.
58
+ - React hooks without providers: `useStorage`, `useStorageSelector`, and `useSetStorage`.
59
+ - Batch reads/writes, namespace cleanup, raw import/export, transactions, and migrations.
60
+ - Web parity with configurable Disk/Secure backends and an IndexedDB backend.
61
+ - MMKV migration helper for moving existing keys without rewriting app code first.
62
+
63
+ ## Install
64
+
65
+ ```sh
65
66
  bun add react-native-nitro-storage react-native-nitro-modules
66
67
  ```
67
68
 
68
- or:
69
+ Use the equivalent command for npm, Yarn, or pnpm if your app does not use Bun.
69
70
 
70
- ```bash
71
- npm install react-native-nitro-storage react-native-nitro-modules
72
- ```
73
-
74
- ### Expo
71
+ For Expo projects, install the native packages, add the config plugin, and prebuild:
75
72
 
76
- ```bash
73
+ ```sh
77
74
  bunx expo install react-native-nitro-storage react-native-nitro-modules
78
75
  ```
79
76
 
80
- Add the config plugin to `app.json`:
81
-
82
77
  ```json
83
78
  {
84
79
  "expo": {
@@ -86,8 +81,8 @@ Add the config plugin to `app.json`:
86
81
  [
87
82
  "react-native-nitro-storage",
88
83
  {
89
- "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID for secure authentication",
90
- "addBiometricPermissions": false
84
+ "faceIDPermission": "Allow $(PRODUCT_NAME) to protect your secure data with Face ID",
85
+ "addBiometricPermissions": true
91
86
  }
92
87
  ]
93
88
  ]
@@ -95,915 +90,295 @@ Add the config plugin to `app.json`:
95
90
  }
96
91
  ```
97
92
 
98
- > `faceIDPermission` sets `NSFaceIDUsageDescription` only when missing. Android biometric permissions are opt-in via `addBiometricPermissions: true`.
99
-
100
- Then run:
101
-
102
- ```bash
93
+ ```sh
103
94
  bunx expo prebuild
104
95
  ```
105
96
 
106
- ### Bare React Native
97
+ The Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, and initializes the Android storage adapter in `MainApplication`.
107
98
 
108
- **iOS:**
99
+ Bare React Native projects should install pods after adding the package:
109
100
 
110
- ```bash
101
+ ```sh
111
102
  cd ios && pod install
112
103
  ```
113
104
 
114
- **Android** initialize the native adapter in `MainApplication.kt`:
105
+ Bare Android apps must initialize the adapter from `MainApplication` before using native storage:
115
106
 
116
- ```kotlin
107
+ ```kt
117
108
  import com.nitrostorage.AndroidStorageAdapter
118
109
 
119
- class MainApplication : Application() {
120
- override fun onCreate() {
121
- super.onCreate()
122
- AndroidStorageAdapter.init(this)
123
- }
110
+ override fun onCreate() {
111
+ super.onCreate()
112
+ AndroidStorageAdapter.init(this)
124
113
  }
125
114
  ```
126
115
 
127
- ---
128
-
129
116
  ## Quick Start
130
117
 
118
+ Create storage items outside React render functions, then use them from anywhere.
119
+
131
120
  ```ts
132
- import { createStorageItem, StorageScope, useStorage } from "react-native-nitro-storage";
121
+ import {
122
+ createStorageItem,
123
+ StorageScope,
124
+ useStorage,
125
+ } from "react-native-nitro-storage";
126
+
127
+ type Theme = "system" | "light" | "dark";
133
128
 
134
- // define a storage item outside of components
135
- const counterItem = createStorageItem({
136
- key: "counter",
137
- scope: StorageScope.Memory,
138
- defaultValue: 0,
129
+ export const themeItem = createStorageItem<Theme>({
130
+ key: "theme",
131
+ scope: StorageScope.Disk,
132
+ defaultValue: "system",
133
+ validate: (value): value is Theme =>
134
+ value === "system" || value === "light" || value === "dark",
139
135
  });
140
136
 
141
- export function Counter() {
142
- const [count, setCount] = useStorage(counterItem);
137
+ export function ThemeButton() {
138
+ const [theme, setTheme] = useStorage(themeItem);
143
139
 
144
140
  return (
145
141
  <Button
146
- title={`Count: ${count}`}
147
- onPress={() => setCount((prev) => prev + 1)}
142
+ title={`Theme: ${theme}`}
143
+ onPress={() => setTheme(theme === "dark" ? "light" : "dark")}
148
144
  />
149
145
  );
150
146
  }
151
147
  ```
152
148
 
153
- ---
154
-
155
- ## Storage Scopes
156
-
157
- | Scope | Backend (iOS) | Backend (Android) | Backend (Web) | Persisted |
158
- | -------- | ------------------------ | -------------------------- | ------------------------------------------------ | --------- |
159
- | `Memory` | In-process JS Map | In-process JS Map | In-process JS Map | No |
160
- | `Disk` | UserDefaults (app suite) | SharedPreferences | `localStorage` | Yes |
161
- | `Secure` | Keychain (AES-256 GCM) | EncryptedSharedPreferences | `localStorage` (`__secure_` + `__bio_` prefixes) | Yes |
162
-
163
- ```ts
164
- import { StorageScope } from "react-native-nitro-storage";
165
-
166
- StorageScope.Memory; // 0 — ephemeral, fastest
167
- StorageScope.Disk; // 1 — persistent, fast
168
- StorageScope.Secure; // 2 — encrypted, slightly slower
169
- ```
170
-
171
- ---
172
-
173
- ## API Reference
174
-
175
- ### `createStorageItem<T>(config)`
176
-
177
- The core factory. Creates a reactive storage item that can be used standalone or with hooks.
178
-
179
- ```ts
180
- function createStorageItem<T = undefined>(
181
- config: StorageItemConfig<T>,
182
- ): StorageItem<T>;
183
- ```
184
-
185
- **Config options:**
186
-
187
- | Property | Type | Default | Description |
188
- | ---------------------- | -------------------------------- | -------------- | -------------------------------------------------------------- |
189
- | `key` | `string` | _required_ | Storage key identifier |
190
- | `scope` | `StorageScope` | _required_ | Where to store the data |
191
- | `defaultValue` | `T` | `undefined` | Value returned when no data exists |
192
- | `serialize` | `(value: T) => string` | JSON fast path | Custom serialization |
193
- | `deserialize` | `(value: string) => T` | JSON fast path | Custom deserialization |
194
- | `validate` | `(value: unknown) => value is T` | — | Type guard run on every read |
195
- | `onValidationError` | `(invalidValue: unknown) => T` | — | Recovery function when validation fails |
196
- | `expiration` | `{ ttlMs: number }` | — | Time-to-live in milliseconds |
197
- | `onExpired` | `(key: string) => void` | — | Callback fired when a TTL value expires on read |
198
- | `readCache` | `boolean` | `false` | Cache deserialized values in JS (avoids repeated native reads) |
199
- | `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
200
- | `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
201
- | `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
202
- | `biometricLevel` | `BiometricLevel` | `None` | Biometric policy (`BiometryOrPasscode` / `BiometryOnly`) |
203
- | `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
204
-
205
- **Returned `StorageItem<T>`:**
206
-
207
- | Method / Property | Type | Description |
208
- | ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
209
- | `get()` | `() => T` | Read current value (synchronous) |
210
- | `getWithVersion()` | `() => { value: T; version: StorageVersion }` | Read value plus current storage version token |
211
- | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
212
- | `setIfVersion(...)` | `(version: StorageVersion, value: T \| ((prev: T) => T)) => boolean` | Write only if version matches (optimistic concurrency) |
213
- | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
214
- | `has()` | `() => boolean` | Check if a value exists in storage |
215
- | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
216
- | `serialize` | `(v: T) => string` | The item's serializer |
217
- | `deserialize` | `(v: string) => T` | The item's deserializer |
218
- | `scope` | `StorageScope` | The item's scope |
219
- | `key` | `string` | The resolved key (includes namespace prefix) |
220
-
221
- **Non-React subscription example:**
222
-
223
- ```ts
224
- const unsubscribe = sessionItem.subscribe(() => {
225
- console.log("session changed:", sessionItem.get());
226
- });
227
-
228
- sessionItem.set("next-session");
229
- unsubscribe();
230
- ```
231
-
232
- ---
233
-
234
- ### React Hooks
235
-
236
- #### `useStorage(item)`
237
-
238
- Full reactive binding. Re-renders when the value changes.
239
-
240
- ```ts
241
- const [value, setValue] = useStorage(item);
242
- ```
243
-
244
- #### `useStorageSelector(item, selector, isEqual?)`
245
-
246
- Subscribe to a derived slice. Only re-renders when the selected value changes.
247
-
248
- ```ts
249
- const [theme, setSettings] = useStorageSelector(settingsItem, (s) => s.theme);
250
- ```
251
-
252
- #### `useSetStorage(item)`
253
-
254
- Write-only hook. Useful when a component needs to update a value but doesn't depend on it.
255
-
256
- ```ts
257
- const setToken = useSetStorage(tokenItem);
258
- setToken("new-token");
259
- ```
260
-
261
- ---
262
-
263
- ### `storage` — Global Utilities
264
-
265
- ```ts
266
- import { storage, StorageScope } from "react-native-nitro-storage";
267
- ```
268
-
269
- | Method | Description |
270
- | ---------------------------------------- | ---------------------------------------------------------------------------- |
271
- | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
272
- | `storage.clearAll()` | Clear Memory + Disk + Secure |
273
- | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
274
- | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
275
- | `storage.has(key, scope)` | Check if a key exists |
276
- | `storage.getAllKeys(scope)` | Get all key names |
277
- | `storage.getKeysByPrefix(prefix, scope)` | Get keys that start with a prefix |
278
- | `storage.getByPrefix(prefix, scope)` | Get raw key-value pairs for keys matching a prefix |
279
- | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
280
- | `storage.size(scope)` | Number of stored keys |
281
- | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
282
- | `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
283
- | `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
284
- | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
285
- | `storage.getString(key, scope)` | Read a raw string value directly (bypasses serialization) |
286
- | `storage.setString(key, value, scope)` | Write a raw string value directly (bypasses serialization) |
287
- | `storage.deleteString(key, scope)` | Delete a raw string value by key |
288
- | `storage.import(data, scope)` | Bulk-load a `Record<string, string>` of raw key/value pairs into a scope |
289
- | `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
290
- | `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
291
- | `storage.resetMetrics()` | Reset in-memory metrics counters |
292
-
293
- > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
294
-
295
- #### Global utility examples
149
+ Secure values use the same item API:
296
150
 
297
151
  ```ts
298
152
  import {
299
153
  AccessControl,
300
- storage,
154
+ BiometricLevel,
155
+ createStorageItem,
301
156
  StorageScope,
302
157
  } from "react-native-nitro-storage";
303
158
 
304
- storage.has("session", StorageScope.Disk);
305
- storage.getAllKeys(StorageScope.Disk);
306
- storage.getKeysByPrefix("user-42:", StorageScope.Disk);
307
- storage.getByPrefix("user-42:", StorageScope.Disk);
308
- storage.getAll(StorageScope.Disk);
309
- storage.size(StorageScope.Disk);
310
-
311
- storage.clearNamespace("user-42", StorageScope.Disk);
312
- storage.clearBiometric();
313
-
314
- storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
315
- storage.setKeychainAccessGroup("group.com.example.shared");
316
-
317
- storage.clear(StorageScope.Memory);
318
- storage.clearAll();
319
- ```
320
-
321
- #### Android secure write mode
322
-
323
- `storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
324
- Use this for non-critical secure writes when lower latency matters more than immediate durability.
325
-
326
- Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
327
-
328
- ```ts
329
- import { storage } from "react-native-nitro-storage";
330
-
331
- storage.setSecureWritesAsync(true);
332
-
333
- // ...multiple secure writes happen (including coalesced item writes)
334
-
335
- storage.flushSecureWrites(); // deterministic durability boundary
336
- ```
337
-
338
- #### Custom web secure backend
339
-
340
- By default, web Secure scope uses `localStorage` with `__secure_` key prefixing. You can replace it with a custom backend (for example encrypted IndexedDB adapter).
341
-
342
- ```ts
343
- import {
344
- getWebSecureStorageBackend,
345
- setWebSecureStorageBackend,
346
- } from "react-native-nitro-storage";
347
-
348
- setWebSecureStorageBackend({
349
- getItem: (key) => encryptedStore.get(key) ?? null,
350
- setItem: (key, value) => encryptedStore.set(key, value),
351
- removeItem: (key) => encryptedStore.delete(key),
352
- clear: () => encryptedStore.clear(),
353
- getAllKeys: () => encryptedStore.keys(),
159
+ export const refreshTokenItem = createStorageItem<string>({
160
+ key: "refreshToken",
161
+ namespace: "auth",
162
+ scope: StorageScope.Secure,
163
+ defaultValue: "",
164
+ biometric: true,
165
+ biometricLevel: BiometricLevel.BiometryOrPasscode,
166
+ accessControl: AccessControl.AfterFirstUnlockThisDeviceOnly,
354
167
  });
355
168
 
356
- const backend = getWebSecureStorageBackend();
357
- console.log("custom backend active:", backend !== undefined);
169
+ refreshTokenItem.set("opaque-refresh-token");
358
170
  ```
359
171
 
360
- ---
361
-
362
- ### IndexedDB Backend for Web
363
-
364
- The default web Secure backend uses `localStorage`, which is synchronous and size-limited. For large payloads or when you need true persistence across tab reloads, use the built-in IndexedDB-backed factory.
172
+ Bootstrap several values in one synchronous call:
365
173
 
366
174
  ```ts
367
- import { setWebSecureStorageBackend } from "react-native-nitro-storage";
368
- import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
369
-
370
- // call once at app startup, before rendering any components that read secure items
371
- const backend = await createIndexedDBBackend();
372
- setWebSecureStorageBackend(backend);
373
- ```
374
-
375
- **How it works:**
175
+ import { StorageScope, getBatch } from "react-native-nitro-storage";
376
176
 
377
- - **Async init**: `createIndexedDBBackend()` opens (or creates) the IndexedDB database and hydrates an in-memory cache from all stored entries before resolving.
378
- - **Synchronous reads**: all `getItem` calls are served from the in-memory cache — no async overhead after init.
379
- - **Fire-and-forget writes**: `setItem`, `removeItem`, and `clear` update the cache synchronously, then persist to IndexedDB in the background. The cache is always the authoritative source.
380
- - **Custom database/store**: optionally pass `dbName` and `storeName` to isolate databases per environment or tenant.
381
-
382
- ```ts
383
- const backend = await createIndexedDBBackend("my-app-db", "secure-kv");
384
- setWebSecureStorageBackend(backend);
177
+ const [theme] = getBatch([themeItem], StorageScope.Disk);
385
178
  ```
386
179
 
387
- ---
388
-
389
- ### `createSecureAuthStorage<K>(config, options?)`
390
-
391
- One-liner factory for authentication flows. Creates multiple `StorageItem<string>` entries in Secure scope.
180
+ Keep multi-step local writes reversible:
392
181
 
393
182
  ```ts
394
- function createSecureAuthStorage<K extends string>(
395
- config: SecureAuthStorageConfig<K>,
396
- options?: { namespace?: string },
397
- ): Record<K, StorageItem<string>>;
398
- ```
399
-
400
- - Default namespace: `"auth"`
401
- - Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
402
- - Supports per-key TTL, biometric level policy, and access control
403
-
404
- ---
405
-
406
- ### Batch Operations
407
-
408
- Atomic multi-key operations. Uses native batch APIs for best performance.
409
-
410
- ```ts
411
- import { getBatch, setBatch, removeBatch } from "react-native-nitro-storage";
412
-
413
- // Read multiple items at once
414
- const [a, b, c] = getBatch([itemA, itemB, itemC], StorageScope.Disk);
415
-
416
- // Write multiple items atomically
417
- setBatch(
418
- [
419
- { item: itemA, value: "hello" },
420
- { item: itemB, value: "world" },
421
- ],
422
- StorageScope.Disk,
423
- );
424
-
425
- // Remove multiple items
426
- removeBatch([itemA, itemB], StorageScope.Disk);
427
- ```
428
-
429
- > All items in a batch must share the same scope. Items with `validate` or `expiration` automatically use per-item paths to preserve semantics.
430
-
431
- ---
432
-
433
- ### Transactions
434
-
435
- Grouped writes with automatic rollback on error.
436
-
437
- ```ts
438
- import { runTransaction, StorageScope } from "react-native-nitro-storage";
183
+ import { StorageScope, runTransaction } from "react-native-nitro-storage";
439
184
 
440
185
  runTransaction(StorageScope.Disk, (tx) => {
441
- const balance = tx.getItem(balanceItem);
442
- tx.setItem(balanceItem, balance - 50);
443
- tx.setItem(logItem, `Deducted 50 at ${new Date().toISOString()}`);
444
-
445
- if (balance - 50 < 0) throw new Error("Insufficient funds");
446
- // if this throws, both writes are rolled back
447
- });
448
- ```
449
-
450
- **TransactionContext methods:**
451
-
452
- | Method | Description |
453
- | ---------------------- | --------------------------- |
454
- | `getItem(item)` | Read a StorageItem's value |
455
- | `setItem(item, value)` | Write a StorageItem's value |
456
- | `removeItem(item)` | Delete a StorageItem |
457
- | `getRaw(key)` | Read raw string by key |
458
- | `setRaw(key, value)` | Write raw string by key |
459
- | `removeRaw(key)` | Delete raw key |
460
-
461
- ---
462
-
463
- ### Migrations
464
-
465
- Versioned, sequential data migrations.
466
-
467
- ```ts
468
- import {
469
- registerMigration,
470
- migrateToLatest,
471
- StorageScope,
472
- } from "react-native-nitro-storage";
473
-
474
- registerMigration(1, ({ setRaw }) => {
475
- setRaw("onboarding-complete", "false");
476
- });
477
-
478
- registerMigration(2, ({ getRaw, setRaw, removeRaw }) => {
479
- const raw = getRaw("legacy-key");
480
- if (raw) {
481
- setRaw("new-key", raw);
482
- removeRaw("legacy-key");
483
- }
484
- });
485
-
486
- // apply all pending migrations (runs once per scope)
487
- migrateToLatest(StorageScope.Disk);
488
- ```
489
-
490
- - Versions must be positive integers, registered in any order, applied ascending
491
- - Version state is tracked per scope via `__nitro_storage_migration_version__`
492
- - Duplicate versions throw at registration time
493
-
494
- ---
495
-
496
- ### MMKV Migration
497
-
498
- Drop-in helper for migrating from `react-native-mmkv`.
499
-
500
- ```ts
501
- import { migrateFromMMKV } from "react-native-nitro-storage";
502
- import { MMKV } from "react-native-mmkv";
503
-
504
- const mmkv = new MMKV();
505
-
506
- const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
507
- // true → value found and copied, original deleted from MMKV
508
- // false → no matching key in MMKV
509
- ```
510
-
511
- - Read priority: `getString` → `getNumber` → `getBoolean`
512
- - Uses `item.set()` so validation still applies
513
- - Only deletes from MMKV when migration succeeds
514
-
515
- ---
516
-
517
- ### Raw String API
518
-
519
- For cases where you want to bypass `createStorageItem` serialization entirely and work with raw key/value strings:
520
-
521
- ```ts
522
- import { storage, StorageScope } from "react-native-nitro-storage";
523
-
524
- storage.setString("raw-key", "raw-value", StorageScope.Disk);
525
- const value = storage.getString("raw-key", StorageScope.Disk); // "raw-value" | undefined
526
- storage.deleteString("raw-key", StorageScope.Disk);
527
- ```
528
-
529
- These are synchronous and go directly to the native backend without any serialize/deserialize step.
530
-
531
- ---
532
-
533
- ### `isKeychainLockedError(err)`
534
-
535
- Utility to detect iOS Keychain locked errors and Android key invalidation errors in secure storage operations. Returns `true` if the error was caused by a locked keychain (device locked, first unlock not yet performed, etc.) or an Android `KeyPermanentlyInvalidatedException` / `InvalidKeyException`. Always returns `false` on web.
536
-
537
- ```ts
538
- import { isKeychainLockedError } from "react-native-nitro-storage";
539
-
540
- try {
541
- secureItem.get();
542
- } catch (err) {
543
- if (isKeychainLockedError(err)) {
544
- // device is locked — retry after unlock
545
- }
546
- }
547
- ```
548
-
549
- ---
550
-
551
- ### Enums
552
-
553
- #### `AccessControl`
554
-
555
- Controls keychain item access requirements (iOS Keychain / Android Keystore). No-op on web.
556
-
557
- ```ts
558
- enum AccessControl {
559
- WhenUnlocked = 0,
560
- AfterFirstUnlock = 1,
561
- WhenPasscodeSetThisDeviceOnly = 2,
562
- WhenUnlockedThisDeviceOnly = 3,
563
- AfterFirstUnlockThisDeviceOnly = 4,
564
- }
565
- ```
566
-
567
- #### `BiometricLevel`
568
-
569
- ```ts
570
- enum BiometricLevel {
571
- None = 0,
572
- BiometryOrPasscode = 1,
573
- BiometryOnly = 2,
574
- }
575
- ```
576
-
577
- ---
578
-
579
- ## Use Cases
580
-
581
- ### Persisted User Preferences
582
-
583
- ```ts
584
- type UserPreferences = {
585
- theme: "light" | "dark" | "system";
586
- language: string;
587
- notifications: boolean;
588
- };
589
-
590
- const prefsItem = createStorageItem<UserPreferences>({
591
- key: "prefs",
592
- scope: StorageScope.Disk,
593
- defaultValue: { theme: "system", language: "en", notifications: true },
186
+ tx.setItem(themeItem, "dark");
187
+ tx.setRaw("onboarding:complete", "true");
594
188
  });
595
-
596
- // in a component — only re-renders when theme changes
597
- const [theme, setPrefs] = useStorageSelector(prefsItem, (p) => p.theme);
598
189
  ```
599
190
 
600
- ### Auth Token Management
601
-
602
- ```ts
603
- const auth = createSecureAuthStorage(
604
- {
605
- accessToken: { ttlMs: 15 * 60_000, biometric: true },
606
- refreshToken: { ttlMs: 7 * 24 * 60 * 60_000 },
607
- idToken: {},
608
- },
609
- { namespace: "myapp-auth" },
610
- );
611
-
612
- // after login
613
- auth.accessToken.set(response.accessToken);
614
- auth.refreshToken.set(response.refreshToken);
615
- auth.idToken.set(response.idToken);
616
-
617
- // check if token exists and hasn't expired
618
- if (auth.accessToken.has()) {
619
- const token = auth.accessToken.get();
620
- // use token
621
- } else {
622
- // refresh or re-login
623
- }
191
+ ## Storage Scopes
624
192
 
625
- // logout
626
- storage.clearNamespace("myapp-auth", StorageScope.Secure);
627
- ```
193
+ | Scope | Backing store | Best for |
194
+ | --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------ |
195
+ | `StorageScope.Memory` | JS memory | Session flags, wizard state, optimistic UI cache |
196
+ | `StorageScope.Disk` | UserDefaults on iOS, SharedPreferences on Android, web Disk backend | Preferences, feature flags, non-secret app state |
197
+ | `StorageScope.Secure` | iOS Keychain, Android Keystore/EncryptedSharedPreferences, web Secure backend | Tokens, credentials, device-bound secrets |
628
198
 
629
- ### Feature Flags with Validation
199
+ Use Secure scope only for secrets. Disk scope is faster and easier to inspect, but it is not a secret store.
630
200
 
631
- ```ts
632
- type FeatureFlags = {
633
- darkMode: boolean;
634
- betaFeature: boolean;
635
- maxUploadMb: number;
636
- };
201
+ ## Docs
637
202
 
638
- const isRecord = (value: unknown): value is Record<string, unknown> =>
639
- typeof value === "object" && value !== null;
203
+ | Task | Start here |
204
+ | ------------------------------------------- | ------------------------------------------------------------------------------ |
205
+ | Learn the public API | [docs/api-reference.md](docs/api-reference.md) |
206
+ | Bind storage to React | [docs/react-hooks.md](docs/react-hooks.md) |
207
+ | Store tokens or biometric secrets | [docs/secure-storage.md](docs/secure-storage.md) |
208
+ | Use batch APIs, transactions, or migrations | [docs/batch-transactions-migrations.md](docs/batch-transactions-migrations.md) |
209
+ | Configure web Disk/Secure storage | [docs/web-backends.md](docs/web-backends.md) |
210
+ | Migrate from `react-native-mmkv` | [docs/mmkv-migration.md](docs/mmkv-migration.md) |
211
+ | Copy working snippets | [docs/recipes.md](docs/recipes.md) |
212
+ | Run or interpret benchmarks | [docs/benchmarks.md](docs/benchmarks.md) |
213
+ | Report a vulnerability | [SECURITY.md](SECURITY.md) |
640
214
 
641
- const isFeatureFlags = (value: unknown): value is FeatureFlags => {
642
- if (!isRecord(value)) return false;
643
- return (
644
- typeof value.darkMode === "boolean" &&
645
- typeof value.betaFeature === "boolean" &&
646
- typeof value.maxUploadMb === "number"
647
- );
648
- };
649
-
650
- const flagsItem = createStorageItem<FeatureFlags>({
651
- key: "feature-flags",
652
- scope: StorageScope.Disk,
653
- defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
654
- validate: isFeatureFlags,
655
- onValidationError: () => ({
656
- darkMode: false,
657
- betaFeature: false,
658
- maxUploadMb: 10,
659
- }),
660
- expiration: { ttlMs: 60 * 60_000 }, // refresh from server every hour
661
- onExpired: () => fetchAndStoreFlags(),
662
- });
663
- ```
664
-
665
- ### Biometric-protected Secrets
215
+ ## API Snapshot
666
216
 
667
217
  ```ts
668
218
  import {
669
219
  AccessControl,
670
220
  BiometricLevel,
671
- createStorageItem,
672
221
  StorageScope,
673
- } from "react-native-nitro-storage";
674
-
675
- const paymentPin = createStorageItem<string>({
676
- key: "payment-pin",
677
- scope: StorageScope.Secure,
678
- defaultValue: "",
679
- biometricLevel: BiometricLevel.BiometryOnly,
680
- accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
681
- });
682
-
683
- paymentPin.set("4829");
684
- const pin = paymentPin.get();
685
- paymentPin.delete();
686
- ```
687
-
688
- ### Multi-Tenant / Namespaced Storage
689
-
690
- ```ts
691
- function createUserStorage(userId: string) {
692
- return {
693
- cart: createStorageItem<string[]>({
694
- key: "cart",
695
- scope: StorageScope.Disk,
696
- defaultValue: [],
697
- namespace: `user-${userId}`,
698
- }),
699
- draft: createStorageItem<string>({
700
- key: "draft",
701
- scope: StorageScope.Disk,
702
- defaultValue: "",
703
- namespace: `user-${userId}`,
704
- }),
705
- };
706
- }
707
-
708
- // clear all data for a specific user
709
- storage.clearNamespace("user-123", StorageScope.Disk);
710
- ```
711
-
712
- ### OTP / Temporary Codes
713
-
714
- ```ts
715
- const otpItem = createStorageItem<string>({
716
- key: "otp-code",
717
- scope: StorageScope.Secure,
718
- defaultValue: "",
719
- expiration: { ttlMs: 5 * 60_000 }, // 5 minutes
720
- onExpired: (key) => {
721
- console.log(`${key} expired — prompt user to request a new code`);
722
- },
723
- });
724
-
725
- // store the code
726
- otpItem.set("482917");
727
-
728
- // later — returns "" if expired
729
- const code = otpItem.get();
730
- ```
731
-
732
- ### Bulk Bootstrap with Batch APIs
733
-
734
- ```ts
735
- import {
222
+ createSecureAuthStorage,
736
223
  createStorageItem,
224
+ flushWebStorageBackends,
737
225
  getBatch,
226
+ getStorageErrorCode,
227
+ isKeychainLockedError,
228
+ migrateFromMMKV,
229
+ migrateToLatest,
230
+ registerMigration,
738
231
  removeBatch,
232
+ runTransaction,
739
233
  setBatch,
740
- StorageScope,
234
+ setWebDiskStorageBackend,
235
+ setWebSecureStorageBackend,
236
+ storage,
237
+ useSetStorage,
238
+ useStorage,
239
+ useStorageSelector,
741
240
  } from "react-native-nitro-storage";
742
-
743
- const firstName = createStorageItem({
744
- key: "first-name",
745
- scope: StorageScope.Disk,
746
- defaultValue: "",
747
- });
748
- const lastName = createStorageItem({
749
- key: "last-name",
750
- scope: StorageScope.Disk,
751
- defaultValue: "",
752
- });
753
-
754
- setBatch(
755
- [
756
- { item: firstName, value: "Ada" },
757
- { item: lastName, value: "Lovelace" },
758
- ],
759
- StorageScope.Disk,
760
- );
761
-
762
- const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
763
- removeBatch([firstName, lastName], StorageScope.Disk);
764
241
  ```
765
242
 
766
- ### Atomic Balance Transfer
243
+ The main building blocks are:
767
244
 
768
- ```ts
769
- const fromBalance = createStorageItem({
770
- key: "from",
771
- scope: StorageScope.Disk,
772
- defaultValue: 100,
773
- });
774
- const toBalance = createStorageItem({
775
- key: "to",
776
- scope: StorageScope.Disk,
777
- defaultValue: 0,
778
- });
245
+ - `createStorageItem<T>(config)` for typed values.
246
+ - `storage` for raw reads, namespace cleanup, secure metadata, metrics, and runtime capability checks.
247
+ - `getBatch`, `setBatch`, and `removeBatch` for multi-key work.
248
+ - `runTransaction` for synchronous rollback on failure.
249
+ - `registerMigration` and `migrateToLatest` for versioned local data migrations.
250
+ - `createSecureAuthStorage` for a compact secure-token item map.
251
+ - `setWebDiskStorageBackend`, `setWebSecureStorageBackend`, and `createIndexedDBBackend` for web persistence.
779
252
 
780
- function transfer(amount: number) {
781
- runTransaction(StorageScope.Disk, (tx) => {
782
- const from = tx.getItem(fromBalance);
783
- if (from < amount) throw new Error("Insufficient funds");
784
-
785
- tx.setItem(fromBalance, from - amount);
786
- tx.setItem(toBalance, tx.getItem(toBalance) + amount);
787
- });
788
- }
789
- ```
253
+ See the full [API reference](docs/api-reference.md).
790
254
 
791
- ### Custom Binary Codec
255
+ ## Platform Support
792
256
 
793
- ```ts
794
- const compactItem = createStorageItem<{ id: number; active: boolean }>({
795
- key: "compact",
796
- scope: StorageScope.Disk,
797
- defaultValue: { id: 0, active: false },
798
- serialize: (v) => `${v.id}|${v.active ? "1" : "0"}`,
799
- deserialize: (v) => {
800
- const [id, flag] = v.split("|");
801
- return { id: Number(id), active: flag === "1" };
802
- },
803
- });
804
- ```
257
+ | Platform | Status | Notes |
258
+ | -------- | --------- | ------------------------------------------------------------------------------------------------------ |
259
+ | iOS | Supported | Disk uses app-suite `UserDefaults`; Secure uses Keychain. |
260
+ | Android | Supported | Disk uses SharedPreferences; Secure uses Keystore-backed EncryptedSharedPreferences. |
261
+ | Expo | Supported | Add the included config plugin before prebuild. |
262
+ | Web | Supported | Defaults to localStorage-style backends; IndexedDB backend is available for persistent Secure storage. |
805
263
 
806
- ### Coalesced Secure Writes with Deterministic Flush
264
+ Peer dependencies:
807
265
 
808
- ```ts
809
- import {
810
- createStorageItem,
811
- storage,
812
- StorageScope,
813
- } from "react-native-nitro-storage";
266
+ - `react >=18.2.0`
267
+ - `react-native >=0.75.0`
268
+ - `react-native-nitro-modules >=0.35.4`
814
269
 
815
- const sessionToken = createStorageItem<string>({
816
- key: "session-token",
817
- scope: StorageScope.Secure,
818
- defaultValue: "",
819
- coalesceSecureWrites: true,
820
- });
270
+ ## Security Model
821
271
 
822
- sessionToken.set("token-v1");
823
- sessionToken.set("token-v2");
824
-
825
- // force pending secure writes to native persistence
826
- storage.flushSecureWrites();
827
- ```
828
-
829
- ### Bulk Data Import
830
-
831
- Load server-fetched data into storage in one atomic call. All keys become visible simultaneously before any listener fires.
272
+ Secure scope stores values in native secure storage on iOS and Android. Biometric items are stored through separate biometric paths and can require biometric or passcode access on each read.
832
273
 
833
274
  ```ts
834
- import { storage, StorageScope } from "react-native-nitro-storage";
835
-
836
- // seed local cache from a server response
837
- const serverData = await fetchInitialData(); // Record<string, string>
838
- storage.import(serverData, StorageScope.Disk);
839
-
840
- // all imported keys are immediately readable
841
- const value = storage.has("remote-config", StorageScope.Disk);
275
+ const capabilities = storage.getSecurityCapabilities();
276
+ const metadata = storage.getSecureMetadata("auth:refreshToken");
842
277
  ```
843
278
 
844
- > `storage.import` writes raw string values directly serialization is bypassed. Use it for bootstrapping data that was already serialized by the server or exported via `storage.getAll`.
279
+ Security metadata APIs never return stored values. They are intended for diagnostics, inventory screens, and support tooling that needs to understand which secure backend is active without exposing secrets.
845
280
 
846
- ---
281
+ Important boundaries:
847
282
 
848
- ### Storage Snapshots and Cleanup
283
+ - Disk and Memory scopes are not secret stores.
284
+ - Web Secure storage depends on the backend you configure; browser storage cannot provide iOS Keychain or Android Keystore guarantees.
285
+ - Hardware-backed secure storage is reported as `unknown` unless the platform can prove it through the native backend.
286
+ - Secret values should not be logged, exported in diagnostics, or copied into crash reports.
849
287
 
850
- ```ts
851
- import { storage, StorageScope } from "react-native-nitro-storage";
852
-
853
- const diskKeys = storage.getAllKeys(StorageScope.Disk);
854
- const diskValues = storage.getAll(StorageScope.Disk);
855
- const secureCount = storage.size(StorageScope.Secure);
288
+ Read [docs/secure-storage.md](docs/secure-storage.md) and [SECURITY.md](SECURITY.md) before using Secure scope for production auth tokens.
856
289
 
857
- if (storage.has("legacy-flag", StorageScope.Disk)) {
858
- storage.clearNamespace("legacy", StorageScope.Disk);
859
- }
860
-
861
- storage.clearBiometric();
862
- ```
290
+ ## Migration Paths
863
291
 
864
- ### Prefix Queries and Namespace Inspection
292
+ From `react-native-mmkv`, migrate existing keys in place and keep your typed item API as the destination:
865
293
 
866
294
  ```ts
867
- import { storage, StorageScope } from "react-native-nitro-storage";
868
-
869
- const userKeys = storage.getKeysByPrefix("user-42:", StorageScope.Disk);
870
- const userRawEntries = storage.getByPrefix("user-42:", StorageScope.Disk);
295
+ import { migrateFromMMKV } from "react-native-nitro-storage";
871
296
 
872
- console.log(userKeys);
873
- console.log(userRawEntries);
297
+ migrateFromMMKV(mmkv, themeItem, true);
298
+ migrateFromMMKV(mmkv, refreshTokenItem, true);
874
299
  ```
875
300
 
876
- ### Optimistic Versioned Writes
301
+ From `AsyncStorage`, `expo-secure-store`, Keychain wrappers, or a custom storage adapter, run a one-time startup migration: read the old value, validate it, write it through the matching `StorageItem`, then stop reading the old key after the migration ships broadly.
877
302
 
878
303
  ```ts
879
- import { createStorageItem, StorageScope } from "react-native-nitro-storage";
880
-
881
- const profileItem = createStorageItem({
882
- key: "profile",
883
- scope: StorageScope.Disk,
884
- defaultValue: { name: "Guest" },
885
- });
304
+ const oldTheme = await AsyncStorage.getItem("theme");
886
305
 
887
- const snapshot = profileItem.getWithVersion();
888
- const didWrite = profileItem.setIfVersion(snapshot.version, {
889
- ...snapshot.value,
890
- name: "Ada",
891
- });
892
-
893
- if (!didWrite) {
894
- // value changed since snapshot; reload and retry
306
+ if (oldTheme === "light" || oldTheme === "dark" || oldTheme === "system") {
307
+ themeItem.set(oldTheme);
308
+ await AsyncStorage.removeItem("theme");
895
309
  }
896
310
  ```
897
311
 
898
- ### Storage Metrics Instrumentation
312
+ For versioned local data, keep migrations explicit and repeatable:
899
313
 
900
314
  ```ts
901
- import { storage } from "react-native-nitro-storage";
902
-
903
- storage.setMetricsObserver((event) => {
904
- console.log(
905
- `[nitro-storage] ${event.operation} scope=${event.scope} duration=${event.durationMs}ms keys=${event.keysCount}`,
906
- );
907
- });
908
-
909
- const metrics = storage.getMetricsSnapshot();
910
- console.log(metrics["item:get"]?.avgDurationMs);
911
-
912
- storage.resetMetrics();
913
- storage.setMetricsObserver(undefined);
914
- ```
915
-
916
- ### Low-level Subscription (outside React)
917
-
918
- ```ts
919
- import { createStorageItem, StorageScope } from "react-native-nitro-storage";
920
-
921
- const notificationsItem = createStorageItem<boolean>({
922
- key: "notifications-enabled",
923
- scope: StorageScope.Disk,
924
- defaultValue: true,
925
- });
315
+ import {
316
+ StorageScope,
317
+ migrateToLatest,
318
+ registerMigration,
319
+ } from "react-native-nitro-storage";
926
320
 
927
- const unsubscribe = notificationsItem.subscribe(() => {
928
- console.log("notifications changed:", notificationsItem.get());
321
+ registerMigration(2, ({ getRaw, setRaw }) => {
322
+ if (getRaw("theme") === "auto") {
323
+ setRaw("theme", "system");
324
+ }
929
325
  });
930
326
 
931
- notificationsItem.set(false);
932
- unsubscribe();
327
+ migrateToLatest(StorageScope.Disk);
933
328
  ```
934
329
 
935
- ### Migrating From MMKV
936
-
937
- ```ts
938
- import { MMKV } from "react-native-mmkv";
330
+ See [docs/mmkv-migration.md](docs/mmkv-migration.md) and [docs/batch-transactions-migrations.md](docs/batch-transactions-migrations.md) for the full flows.
939
331
 
940
- const mmkv = new MMKV();
332
+ ## Choosing a Storage Library
941
333
 
942
- const usernameItem = createStorageItem({
943
- key: "username",
944
- scope: StorageScope.Disk,
945
- defaultValue: "",
946
- });
334
+ | Need | Good fit |
335
+ | --------------------------------------------------------------- | ------------------------------------------- |
336
+ | Fast synchronous typed state plus secure storage in one package | `react-native-nitro-storage` |
337
+ | Existing MMKV-only app with no secure storage requirement | `react-native-mmkv` can still be enough |
338
+ | Async key/value persistence only | `@react-native-async-storage/async-storage` |
339
+ | Expo-only secure token storage with async calls | `expo-secure-store` |
340
+ | Keychain/Keystore credentials only | A focused Keychain wrapper may be simpler |
947
341
 
948
- // run once at app startup
949
- migrateFromMMKV(mmkv, usernameItem, true); // true = delete from MMKV after
950
- ```
342
+ Nitro Storage is strongest when the app needs synchronous reads, React bindings, typed values, secure storage, and migration utilities together. It is not trying to replace databases, query caches, or server-state libraries.
951
343
 
952
- ---
344
+ ## Production Checklist
953
345
 
954
- ## Exported Types
346
+ - Use `StorageScope.Secure` only for secrets and tokens.
347
+ - Keep secrets out of logs, exports, analytics, and crash reports.
348
+ - Test biometric and Keychain/Keystore flows on real iOS and Android devices.
349
+ - Configure a web Secure backend intentionally; browser storage does not provide native Keychain or Keystore guarantees.
350
+ - Run the full package gate before release:
955
351
 
956
- ```ts
957
- import type {
958
- Storage,
959
- StorageItemConfig,
960
- StorageItem,
961
- StorageBatchSetItem,
962
- Validator,
963
- ExpirationConfig,
964
- MigrationContext,
965
- Migration,
966
- TransactionContext,
967
- StorageVersion,
968
- VersionedValue,
969
- StorageMetricsEvent,
970
- StorageMetricsObserver,
971
- StorageMetricSummary,
972
- WebSecureStorageBackend,
973
- MMKVLike,
974
- SecureAuthStorageConfig,
975
- } from "react-native-nitro-storage";
352
+ ```sh
353
+ bun run lint -- --filter=react-native-nitro-storage
354
+ bun run format:check -- --filter=react-native-nitro-storage
355
+ bun run typecheck -- --filter=react-native-nitro-storage
356
+ bun run test:types -- --filter=react-native-nitro-storage
357
+ bun run test -- --filter=react-native-nitro-storage
358
+ bun run test:cpp -- --filter=react-native-nitro-storage
359
+ bun run --cwd packages/react-native-nitro-storage check:pack
360
+ npm publish --dry-run
976
361
  ```
977
362
 
978
- ---
979
-
980
- ## Dev Commands
363
+ ## Development
981
364
 
982
- From repository root:
983
-
984
- ```bash
985
- bun run test -- --filter=react-native-nitro-storage
365
+ ```sh
366
+ bun install
986
367
  bun run lint -- --filter=react-native-nitro-storage
987
368
  bun run format:check -- --filter=react-native-nitro-storage
988
369
  bun run typecheck -- --filter=react-native-nitro-storage
989
370
  bun run test:types -- --filter=react-native-nitro-storage
371
+ bun run test -- --filter=react-native-nitro-storage
990
372
  bun run test:cpp -- --filter=react-native-nitro-storage
991
- bun run build -- --filter=react-native-nitro-storage
992
373
  ```
993
374
 
994
- Inside `packages/react-native-nitro-storage`:
995
-
996
- ```bash
997
- bun run test # run tests
998
- bun run test:coverage # run tests with coverage
999
- bun run lint # eslint (expo-magic rules)
1000
- bun run format:check # prettier check
1001
- bun run typecheck # tsc --noEmit
1002
- bun run test:types # public type-level API tests
1003
- bun run test:cpp # C++ binding/core tests
1004
- bun run check:pack # npm pack content guard
1005
- bun run build # bob build
1006
- bun run benchmark # performance benchmarks
375
+ Release checks:
376
+
377
+ ```sh
378
+ bun run build -- --filter=react-native-nitro-storage
379
+ bun run benchmark -- --filter=react-native-nitro-storage
380
+ bun run --cwd packages/react-native-nitro-storage check:pack
381
+ npm publish --dry-run
1007
382
  ```
1008
383
 
1009
384
  ## License