NativeScope / Storage
Storage providers
How AsyncStorage, MMKV, expo-sqlite and op-sqlite are detected, and what the studio can do with each.
On this page
NativeScope groups everything it can read into providers. Each provider exposes one or more instances — a single AsyncStorage store, each named MMKV instance, each opened SQLite database.
AsyncStorage
Detected from @react-native-async-storage/async-storage. It exposes a single instance, default,
because AsyncStorage is a process-wide singleton.
Everything in AsyncStorage is a string. NativeScope infers whether a value is JSON by parsing it, and labels it accordingly, so JSON values get a structured editor instead of a text box. The stored bytes are never rewritten by that inference — it only affects presentation.
MMKV
Detected from react-native-mmkv. Instances are discovered automatically as your app constructs
them, so an app with a default store and a separate cache store shows both without any
registration step.
MMKV exposes typed getters rather than a schema. NativeScope probes string, number, boolean, then buffer, and promotes strings that parse as JSON. The inferred type is always visible. Changing it is an explicit Convert type action in the key menu, never a silent rewrite.
SQLite
Two SQLite drivers are supported, and they can be used at the same time in one app — each shows up as its own provider in the sidebar:
| Provider | Package | Label in the studio |
|---|---|---|
| expo-sqlite | expo-sqlite | SQLite |
| op-sqlite | @op-engineering/op-sqlite | OP-SQLite |
Every database your app opens becomes an instance. For each one you get the table list with row counts, a schema view, an editable row grid, and a SQL console that runs arbitrary statements against the live database on the device.
Views are listed too, grouped separately and named with what they read. They are read, counted,
searched, exported and — when they have the INSTEAD OF triggers SQLite requires — edited, exactly
like tables. That is what makes the inspector usable on a sync engine's local database, where the
app's tables are views over opaque JSON. See SQL views.
Both providers share the same engine-agnostic implementation, so every feature is identical between them. Two differences are inherent to the drivers:
- op-sqlite reports the operation. Its update hook carries
INSERT/UPDATE/DELETE, so the activity feed names what happened. The expo-sqlite hook only reports which row changed, so those events read as a generic change. - op-sqlite returns BLOBs as
ArrayBuffer, expo-sqlite as a byte array. Either way the grid shows the column as(blob, 7.8 KB)and opens a read-only hex viewer on double-click.
Row counts are exact on ordinary tables; only genuinely large ones fall back to an estimate, always
prefixed with ~. Cells over 4 KB come truncated with the full contents available on demand. Those
mechanics — and the guarantees that keep them from ever corrupting data — live in
Large datasets.
If you use an ORM on top of op-sqlite (Drizzle, for example), its internal database handle is instrumented too — the Metro resolver sees the import regardless of who wrote it.
Change attribution
Every change the studio shows is tagged with its origin: app when your code wrote it, studio
when you did. Rapid bursts of writes to the same key are coalesced into a single event carrying a
count, so a tight write loop cannot flood the activity feed or the JS thread.
Adding another provider
The adapter layer is generic and the search, export and pagination paths are implemented once on top of it rather than per adapter. A new key/value provider implements listing, reading, writing and subscribing; a new SQL database provider usually only has to translate its query method into two functions. Neither touches the protocol.
Adding a database provider walks through it with the real checklist.

