Inspect the embedded databases inside your running Dart or Flutter application, from a desktop client, over a paired connection.
Add one package to your app, forward a port, and browse your live data — no
adb pull, no copying a .db off the device and wondering whether you also
took its -wal.
These are the Apache-2.0 client-side libraries. They are engine-neutral and product-neutral: they speak a documented wire protocol, and any client that speaks it can drive them.
The protocol is not a Dart protocol. A bridge is whatever speaks it, and this repository holds the engine-neutral core for each language it is implemented in, beside the specification that binds all of them.
| Language | Core | Status | Registry |
|---|---|---|---|
| Dart / Flutter | dart/dbinspect_bridge |
shipped | pub.dev dbinspect_bridge |
| JVM — desktop and native Android | jvm/dbinspect-bridge |
M3, not yet written | Maven Central org.dizitart:dbinspect-bridge |
| Rust | rust/dbinspect-bridge |
M4, not yet written | crates.io dbinspect-bridge |
A core depends on no database. It is the protocol, pairing, the transport and the release guard. Everything that knows about an actual storage engine is a separate adapter that depends on a core:
| Adapter | Language | Lives in |
|---|---|---|
dart/dbinspect_sqflite — SqfliteAdapter |
Dart | here |
dbinspect_drift, dbinspect_hive (M8) |
Dart | here |
nitrite_bridge |
Dart | nitrite-flutter packages/ |
org.dizitart:nitrite-bridge |
JVM | nitrite-java nitrite-bridge/ |
nitrite-bridge |
Rust | nitrite-rust nitrite-bridge/ |
An adapter never depends on another adapter, so inspecting a SQLite database pulls in no Nitrite and inspecting a Nitrite database pulls in no SQLite — and, because the core is here rather than inside a database's own repository, you can inspect a JVM or Rust database that has nothing to do with Nitrite by writing an adapter against the core. That is the whole reason the split exists. A JDBC, H2, MapDB, redb or sled adapter is a downstream package; none is scheduled here, and none needs anything from us to be written.
This repository sits in the nitrite organisation, and that says nothing
about what it depends on. It is there because docs/THREAT-MODEL.md §7 is the
acceptance criteria for the three nitrite-bridge adapters above, and a
specification is easier to maintain beside the implementations it binds.
import 'package:dbinspect_bridge/dbinspect_bridge.dart';
import 'package:dbinspect_sqflite/dbinspect_sqflite.dart';
await startBridge(
appName: 'example_app',
adapters: [
SqfliteAdapter(executor: db, id: 'app', displayName: 'app.db'),
],
);The pairing code is printed to your logger in a banner. Type it into the client to connect.
It is not in your release build. startBridge returns null behind a
compile-time constant, so the AOT compiler drops the server and the protocol
strings entirely. Opt back in on purpose with
--dart-define=DBINSPECT_BRIDGE=true.
It binds 127.0.0.1. Reaching it from another machine is meant to be a
deliberate act — adb forward tcp:9000 tcp:9000 or an SSH tunnel. Setting
bindAddress forces TLS with a certificate generated for the session, whose
SHA-256 fingerprint goes in the banner for the client to pin.
Everything beyond reading is off. edit, sql and snapshot are false
and regex is absent unless you opt in per adapter. An operation that is off is
absent from the reported capabilities, not merely refused when called.
Pairing is not a formality. 40 bits, regenerated per run, compared in constant time, with a failure budget that is per bridge session rather than per connection — ten wrong guesses close pairing until the application restarts.
| Document | What it covers |
|---|---|
| docs/PROTOCOL.md | The JSON-RPC wire protocol, frozen at v1. Additive changes only. |
| docs/THREAT-MODEL.md | The threat model. §5 is binding architecture, §7 is binding acceptance criteria. |
Both are binding on every implementation of the protocol, in every language, and
conformance/ is how that is checked rather than asserted: a
black-box suite that pairs with a running bridge and reads what it puts on the
wire, taking a host:port and a pairing code and nothing else. The same
invocation drives the Dart bridge today and the JVM and Rust bridges when they
land — which is what freezing the protocol was for.
cd conformance && dart run bin/dbinspect_conformance.dart 127.0.0.1:53219 J8K4M2QXEach Dart package is plain dart pub — pure Dart, no Flutter, no workspace
wiring. They must stay that way: they are embedded in other people's
applications and requiring a Flutter SDK to build one would be a real cost to a
server-side or CLI user.
for p in dart/*; do (cd "$p" && dart pub get && dart format --output=none --set-exit-if-changed . && dart analyze --fatal-infos && dart test) || break; donejvm/ and rust/ get their own CI jobs when they land. The language is a
directory, not the shape of the repository.
Two checks need more than a Dart SDK and so have jobs of their own.
dart/dbinspect_sqflite/example/flutter_app
is a Flutter app with the bridge in it over a 50k-row table, and it is what they
run against:
cd dart/dbinspect_sqflite/example/flutter_app && ./tool/verify_release_apk.sh
dart tool/measure_page_latency.dart <host:port> <code>The first builds the release APK and greps it for the protocol strings — §7
criterion 2 on a shipped artifact rather than on a probe binary — with the
--dart-define opt-in as its negative control. The second measures what a
client waits for a page, reporting the round trip and the adapter's own share
separately, and runs against a bridge in any language.
conformance/ is a job of its own and is minutes long: one of the
criteria it carries is "ten wrong pairing codes close the gate", and the bridge's
backoff is a real four-minute wait that exists to be that slow.
Fanlight is a desktop client for this protocol
(Windows / macOS / Linux) and is a separate, commercial product. The protocol is
open and these libraries are Apache-2.0; nothing here depends on Fanlight, and
writing another client requires only docs/PROTOCOL.md.
Apache-2.0. See LICENSE.