react-native-nitro-storage 0.4.5 → 0.5.1

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