react-native-nitro-storage 0.4.5 → 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.
- package/README.md +235 -960
- package/SECURITY.md +26 -0
- package/docs/api-reference.md +217 -0
- package/docs/batch-transactions-migrations.md +186 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +281 -0
- package/docs/secure-storage.md +171 -0
- package/docs/web-backends.md +141 -0
- package/lib/commonjs/index.js +51 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +54 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/module/index.js +51 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +54 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/typescript/index.d.ts +5 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +5 -2
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +32 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/package.json +21 -8
- package/src/index.ts +67 -1
- package/src/index.web.ts +76 -0
- package/src/storage-runtime.ts +35 -0
package/README.md
CHANGED
|
@@ -1,88 +1,79 @@
|
|
|
1
1
|
# react-native-nitro-storage
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## Installation
|
|
67
|
-
|
|
68
|
-
```bash
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-nitro-storage)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://reactnative.dev/)
|
|
6
|
+
[](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
|
|
69
66
|
bun add react-native-nitro-storage react-native-nitro-modules
|
|
70
67
|
```
|
|
71
68
|
|
|
72
|
-
or
|
|
69
|
+
Use the equivalent command for npm, Yarn, or pnpm if your app does not use Bun.
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
npm install react-native-nitro-storage react-native-nitro-modules
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Expo
|
|
71
|
+
For Expo projects, install the native packages, add the config plugin, and prebuild:
|
|
79
72
|
|
|
80
|
-
```
|
|
73
|
+
```sh
|
|
81
74
|
bunx expo install react-native-nitro-storage react-native-nitro-modules
|
|
82
75
|
```
|
|
83
76
|
|
|
84
|
-
Add the config plugin to `app.json`:
|
|
85
|
-
|
|
86
77
|
```json
|
|
87
78
|
{
|
|
88
79
|
"expo": {
|
|
@@ -90,8 +81,8 @@ Add the config plugin to `app.json`:
|
|
|
90
81
|
[
|
|
91
82
|
"react-native-nitro-storage",
|
|
92
83
|
{
|
|
93
|
-
"faceIDPermission": "Allow $(PRODUCT_NAME) to
|
|
94
|
-
"addBiometricPermissions":
|
|
84
|
+
"faceIDPermission": "Allow $(PRODUCT_NAME) to protect your secure data with Face ID",
|
|
85
|
+
"addBiometricPermissions": true
|
|
95
86
|
}
|
|
96
87
|
]
|
|
97
88
|
]
|
|
@@ -99,1011 +90,295 @@ Add the config plugin to `app.json`:
|
|
|
99
90
|
}
|
|
100
91
|
```
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Then run:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
93
|
+
```sh
|
|
107
94
|
bunx expo prebuild
|
|
108
95
|
```
|
|
109
96
|
|
|
110
|
-
|
|
97
|
+
The Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, and initializes the Android storage adapter in `MainApplication`.
|
|
111
98
|
|
|
112
|
-
|
|
99
|
+
Bare React Native projects should install pods after adding the package:
|
|
113
100
|
|
|
114
|
-
```
|
|
101
|
+
```sh
|
|
115
102
|
cd ios && pod install
|
|
116
103
|
```
|
|
117
104
|
|
|
118
|
-
|
|
105
|
+
Bare Android apps must initialize the adapter from `MainApplication` before using native storage:
|
|
119
106
|
|
|
120
|
-
```
|
|
107
|
+
```kt
|
|
121
108
|
import com.nitrostorage.AndroidStorageAdapter
|
|
122
109
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
AndroidStorageAdapter.init(this)
|
|
127
|
-
}
|
|
110
|
+
override fun onCreate() {
|
|
111
|
+
super.onCreate()
|
|
112
|
+
AndroidStorageAdapter.init(this)
|
|
128
113
|
}
|
|
129
114
|
```
|
|
130
115
|
|
|
131
|
-
---
|
|
132
|
-
|
|
133
116
|
## Quick Start
|
|
134
117
|
|
|
118
|
+
Create storage items outside React render functions, then use them from anywhere.
|
|
119
|
+
|
|
135
120
|
```ts
|
|
136
|
-
import {
|
|
121
|
+
import {
|
|
122
|
+
createStorageItem,
|
|
123
|
+
StorageScope,
|
|
124
|
+
useStorage,
|
|
125
|
+
} from "react-native-nitro-storage";
|
|
126
|
+
|
|
127
|
+
type Theme = "system" | "light" | "dark";
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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",
|
|
143
135
|
});
|
|
144
136
|
|
|
145
|
-
export function
|
|
146
|
-
const [
|
|
137
|
+
export function ThemeButton() {
|
|
138
|
+
const [theme, setTheme] = useStorage(themeItem);
|
|
147
139
|
|
|
148
140
|
return (
|
|
149
141
|
<Button
|
|
150
|
-
title={`
|
|
151
|
-
onPress={() =>
|
|
142
|
+
title={`Theme: ${theme}`}
|
|
143
|
+
onPress={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
152
144
|
/>
|
|
153
145
|
);
|
|
154
146
|
}
|
|
155
147
|
```
|
|
156
148
|
|
|
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
|
|
149
|
+
Secure values use the same item API:
|
|
312
150
|
|
|
313
151
|
```ts
|
|
314
152
|
import {
|
|
315
153
|
AccessControl,
|
|
316
|
-
|
|
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 {
|
|
154
|
+
BiometricLevel,
|
|
344
155
|
createStorageItem,
|
|
345
|
-
storage,
|
|
346
156
|
StorageScope,
|
|
347
157
|
} from "react-native-nitro-storage";
|
|
348
158
|
|
|
349
|
-
const
|
|
350
|
-
key: "
|
|
351
|
-
|
|
159
|
+
export const refreshTokenItem = createStorageItem<string>({
|
|
160
|
+
key: "refreshToken",
|
|
161
|
+
namespace: "auth",
|
|
162
|
+
scope: StorageScope.Secure,
|
|
352
163
|
defaultValue: "",
|
|
353
|
-
|
|
164
|
+
biometric: true,
|
|
165
|
+
biometricLevel: BiometricLevel.BiometryOrPasscode,
|
|
166
|
+
accessControl: AccessControl.AfterFirstUnlockThisDeviceOnly,
|
|
354
167
|
});
|
|
355
168
|
|
|
356
|
-
|
|
357
|
-
storage.setDiskWritesAsync(true);
|
|
358
|
-
storage.setString("draft:raw", "value", StorageScope.Disk);
|
|
359
|
-
|
|
360
|
-
storage.flushDiskWrites(); // commit queued Disk writes
|
|
361
|
-
storage.setDiskWritesAsync(false);
|
|
169
|
+
refreshTokenItem.set("opaque-refresh-token");
|
|
362
170
|
```
|
|
363
171
|
|
|
364
|
-
|
|
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).
|
|
172
|
+
Bootstrap several values in one synchronous call:
|
|
370
173
|
|
|
371
174
|
```ts
|
|
372
|
-
import {
|
|
175
|
+
import { StorageScope, getBatch } from "react-native-nitro-storage";
|
|
373
176
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// ...multiple secure writes happen (including coalesced item writes)
|
|
377
|
-
|
|
378
|
-
storage.flushSecureWrites(); // deterministic durability boundary
|
|
177
|
+
const [theme] = getBatch([themeItem], StorageScope.Disk);
|
|
379
178
|
```
|
|
380
179
|
|
|
381
|
-
|
|
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:
|
|
180
|
+
Keep multi-step local writes reversible:
|
|
386
181
|
|
|
387
182
|
```ts
|
|
388
|
-
|
|
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";
|
|
183
|
+
import { StorageScope, runTransaction } from "react-native-nitro-storage";
|
|
532
184
|
|
|
533
185
|
runTransaction(StorageScope.Disk, (tx) => {
|
|
534
|
-
|
|
535
|
-
tx.
|
|
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
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
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.
|
|
559
|
-
|
|
560
|
-
```ts
|
|
561
|
-
import {
|
|
562
|
-
registerMigration,
|
|
563
|
-
migrateToLatest,
|
|
564
|
-
StorageScope,
|
|
565
|
-
} from "react-native-nitro-storage";
|
|
566
|
-
|
|
567
|
-
registerMigration(1, ({ setRaw }) => {
|
|
568
|
-
setRaw("onboarding-complete", "false");
|
|
569
|
-
});
|
|
570
|
-
|
|
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);
|
|
581
|
-
```
|
|
582
|
-
|
|
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
|
-
---
|
|
588
|
-
|
|
589
|
-
### MMKV Migration
|
|
590
|
-
|
|
591
|
-
Drop-in helper for migrating from `react-native-mmkv`.
|
|
592
|
-
|
|
593
|
-
```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
|
|
611
|
-
|
|
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 },
|
|
186
|
+
tx.setItem(themeItem, "dark");
|
|
187
|
+
tx.setRaw("onboarding:complete", "true");
|
|
694
188
|
});
|
|
695
|
-
|
|
696
|
-
// in a component — only re-renders when theme changes
|
|
697
|
-
const [theme, setPrefs] = useStorageSelector(prefsItem, (p) => p.theme);
|
|
698
189
|
```
|
|
699
190
|
|
|
700
|
-
|
|
701
|
-
|
|
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: {},
|
|
708
|
-
},
|
|
709
|
-
{ namespace: "myapp-auth" },
|
|
710
|
-
);
|
|
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
|
-
}
|
|
191
|
+
## Storage Scopes
|
|
724
192
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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 |
|
|
728
198
|
|
|
729
|
-
|
|
199
|
+
Use Secure scope only for secrets. Disk scope is faster and easier to inspect, but it is not a secret store.
|
|
730
200
|
|
|
731
|
-
|
|
732
|
-
type FeatureFlags = {
|
|
733
|
-
darkMode: boolean;
|
|
734
|
-
betaFeature: boolean;
|
|
735
|
-
maxUploadMb: number;
|
|
736
|
-
};
|
|
201
|
+
## Docs
|
|
737
202
|
|
|
738
|
-
|
|
739
|
-
|
|
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) |
|
|
740
214
|
|
|
741
|
-
|
|
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
|
-
};
|
|
749
|
-
|
|
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
|
-
```
|
|
764
|
-
|
|
765
|
-
### Biometric-protected Secrets
|
|
215
|
+
## API Snapshot
|
|
766
216
|
|
|
767
217
|
```ts
|
|
768
218
|
import {
|
|
769
219
|
AccessControl,
|
|
770
220
|
BiometricLevel,
|
|
771
|
-
createStorageItem,
|
|
772
221
|
StorageScope,
|
|
773
|
-
|
|
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 {
|
|
222
|
+
createSecureAuthStorage,
|
|
836
223
|
createStorageItem,
|
|
224
|
+
flushWebStorageBackends,
|
|
837
225
|
getBatch,
|
|
226
|
+
getStorageErrorCode,
|
|
227
|
+
isKeychainLockedError,
|
|
228
|
+
migrateFromMMKV,
|
|
229
|
+
migrateToLatest,
|
|
230
|
+
registerMigration,
|
|
838
231
|
removeBatch,
|
|
232
|
+
runTransaction,
|
|
839
233
|
setBatch,
|
|
840
|
-
|
|
234
|
+
setWebDiskStorageBackend,
|
|
235
|
+
setWebSecureStorageBackend,
|
|
236
|
+
storage,
|
|
237
|
+
useSetStorage,
|
|
238
|
+
useStorage,
|
|
239
|
+
useStorageSelector,
|
|
841
240
|
} 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
241
|
```
|
|
865
242
|
|
|
866
|
-
|
|
243
|
+
The main building blocks are:
|
|
867
244
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
key: "to",
|
|
876
|
-
scope: StorageScope.Disk,
|
|
877
|
-
defaultValue: 0,
|
|
878
|
-
});
|
|
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.
|
|
879
252
|
|
|
880
|
-
|
|
881
|
-
runTransaction(StorageScope.Disk, (tx) => {
|
|
882
|
-
const from = tx.getItem(fromBalance);
|
|
883
|
-
if (from < amount) throw new Error("Insufficient funds");
|
|
253
|
+
See the full [API reference](docs/api-reference.md).
|
|
884
254
|
|
|
885
|
-
|
|
886
|
-
tx.setItem(toBalance, tx.getItem(toBalance) + amount);
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
```
|
|
255
|
+
## Platform Support
|
|
890
256
|
|
|
891
|
-
|
|
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. |
|
|
892
263
|
|
|
893
|
-
|
|
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
|
-
```
|
|
264
|
+
Peer dependencies:
|
|
905
265
|
|
|
906
|
-
|
|
266
|
+
- `react >=18.2.0`
|
|
267
|
+
- `react-native >=0.75.0`
|
|
268
|
+
- `react-native-nitro-modules >=0.35.4`
|
|
907
269
|
|
|
908
|
-
|
|
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
|
-
});
|
|
270
|
+
## Security Model
|
|
921
271
|
|
|
922
|
-
|
|
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.
|
|
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.
|
|
932
273
|
|
|
933
274
|
```ts
|
|
934
|
-
|
|
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);
|
|
275
|
+
const capabilities = storage.getSecurityCapabilities();
|
|
276
|
+
const metadata = storage.getSecureMetadata("auth:refreshToken");
|
|
942
277
|
```
|
|
943
278
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
---
|
|
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.
|
|
947
280
|
|
|
948
|
-
|
|
281
|
+
Important boundaries:
|
|
949
282
|
|
|
950
|
-
|
|
951
|
-
|
|
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.
|
|
952
287
|
|
|
953
|
-
|
|
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
|
-
}
|
|
288
|
+
Read [docs/secure-storage.md](docs/secure-storage.md) and [SECURITY.md](SECURITY.md) before using Secure scope for production auth tokens.
|
|
960
289
|
|
|
961
|
-
|
|
962
|
-
```
|
|
290
|
+
## Migration Paths
|
|
963
291
|
|
|
964
|
-
|
|
292
|
+
From `react-native-mmkv`, migrate existing keys in place and keep your typed item API as the destination:
|
|
965
293
|
|
|
966
294
|
```ts
|
|
967
|
-
import {
|
|
968
|
-
|
|
969
|
-
const userKeys = storage.getKeysByPrefix("user-42:", StorageScope.Disk);
|
|
970
|
-
const userRawEntries = storage.getByPrefix("user-42:", StorageScope.Disk);
|
|
295
|
+
import { migrateFromMMKV } from "react-native-nitro-storage";
|
|
971
296
|
|
|
972
|
-
|
|
973
|
-
|
|
297
|
+
migrateFromMMKV(mmkv, themeItem, true);
|
|
298
|
+
migrateFromMMKV(mmkv, refreshTokenItem, true);
|
|
974
299
|
```
|
|
975
300
|
|
|
976
|
-
|
|
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.
|
|
977
302
|
|
|
978
303
|
```ts
|
|
979
|
-
|
|
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
|
-
});
|
|
304
|
+
const oldTheme = await AsyncStorage.getItem("theme");
|
|
992
305
|
|
|
993
|
-
if (
|
|
994
|
-
|
|
306
|
+
if (oldTheme === "light" || oldTheme === "dark" || oldTheme === "system") {
|
|
307
|
+
themeItem.set(oldTheme);
|
|
308
|
+
await AsyncStorage.removeItem("theme");
|
|
995
309
|
}
|
|
996
310
|
```
|
|
997
311
|
|
|
998
|
-
|
|
312
|
+
For versioned local data, keep migrations explicit and repeatable:
|
|
999
313
|
|
|
1000
314
|
```ts
|
|
1001
|
-
import {
|
|
315
|
+
import {
|
|
316
|
+
StorageScope,
|
|
317
|
+
migrateToLatest,
|
|
318
|
+
registerMigration,
|
|
319
|
+
} from "react-native-nitro-storage";
|
|
1002
320
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
321
|
+
registerMigration(2, ({ getRaw, setRaw }) => {
|
|
322
|
+
if (getRaw("theme") === "auto") {
|
|
323
|
+
setRaw("theme", "system");
|
|
324
|
+
}
|
|
1007
325
|
});
|
|
1008
326
|
|
|
1009
|
-
|
|
1010
|
-
console.log(metrics["item:get"]?.avgDurationMs);
|
|
1011
|
-
|
|
1012
|
-
storage.resetMetrics();
|
|
1013
|
-
storage.setMetricsObserver(undefined);
|
|
327
|
+
migrateToLatest(StorageScope.Disk);
|
|
1014
328
|
```
|
|
1015
329
|
|
|
1016
|
-
|
|
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.
|
|
1017
331
|
|
|
1018
|
-
|
|
1019
|
-
import { createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
332
|
+
## Choosing a Storage Library
|
|
1020
333
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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 |
|
|
1026
341
|
|
|
1027
|
-
|
|
1028
|
-
console.log("notifications changed:", notificationsItem.get());
|
|
1029
|
-
});
|
|
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.
|
|
1030
343
|
|
|
1031
|
-
|
|
1032
|
-
unsubscribe();
|
|
1033
|
-
```
|
|
344
|
+
## Production Checklist
|
|
1034
345
|
|
|
1035
|
-
|
|
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:
|
|
1036
351
|
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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";
|
|
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
|
|
1076
361
|
```
|
|
1077
362
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
## Dev Commands
|
|
1081
|
-
|
|
1082
|
-
From repository root:
|
|
363
|
+
## Development
|
|
1083
364
|
|
|
1084
|
-
```
|
|
1085
|
-
bun
|
|
365
|
+
```sh
|
|
366
|
+
bun install
|
|
1086
367
|
bun run lint -- --filter=react-native-nitro-storage
|
|
1087
368
|
bun run format:check -- --filter=react-native-nitro-storage
|
|
1088
369
|
bun run typecheck -- --filter=react-native-nitro-storage
|
|
1089
370
|
bun run test:types -- --filter=react-native-nitro-storage
|
|
371
|
+
bun run test -- --filter=react-native-nitro-storage
|
|
1090
372
|
bun run test:cpp -- --filter=react-native-nitro-storage
|
|
1091
|
-
bun run build -- --filter=react-native-nitro-storage
|
|
1092
373
|
```
|
|
1093
374
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
```
|
|
1097
|
-
bun run
|
|
1098
|
-
bun run
|
|
1099
|
-
bun run
|
|
1100
|
-
|
|
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
|
|
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
|
|
1107
382
|
```
|
|
1108
383
|
|
|
1109
384
|
## License
|