The DBChat Firebase driver & bridge โ a single Node program that speaks
dbchart.bridge v1 over NDJSON stdin/stdout (see
dbchart-vscode/docs/driver-bridge-architecture.md), covering Firestore and
the Realtime Database.
This app merges two projects into one bridge:
| Merged project | Role in the driver |
|---|---|
fires2rest โ Firestore/RTDB REST client |
reads the live backend |
relationalize + rtdb_bridge (Node port) |
turns the JSON tree into relational tables + SQL |
Both are private (not published), so their code is recreated inside this
app under src/vendor/ โ the driver has zero runtime npm dependencies
(only node:* builtins).
live Firebase export tree canonical model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ read โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ convert โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Firestore / RTDB REST โ โโโโโโโโโบ โ { collection: { key: rec } }โ โโโโโโโโโโบ โ relationalize (vendored) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ client โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ envelope
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ adapter: -> DatabaseSchemaโ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โผ
one bridge: {"protocol":"dbchart.bridge","protocolVersion":1, ...}
| Path | Purpose |
|---|---|
src/contracts/ |
protocol constants, frames, manifest, ConnectionConfig, canonical schema, envelope, output validation |
src/bridge/ |
NDJSON transport, method allow-list + dispatch, size caps, stderr logging |
src/clients/ |
fires2rest-backed client factory, credentials, Firestore collection discovery |
src/convert/ |
Converter seam, relationalize-backed converter, the canonical adapter |
src/introspect/ |
live RTDB / Firestore readers โ export tree |
src/driver/ |
the Firebase driver (verbs, connection registry, dialect routing) |
src/vendor/ |
recreations of fires2rest and relationalize/rtdb_bridge |
bin/firebase-bridge.mjs |
the stdio entry the host spawns |
bridge.json |
the manifest the host discovers |
| Verb | Behaviour | Gated by |
|---|---|---|
describe / health |
manifest echo / liveness | always |
convert |
params.payload (RTDB export JSON) โ envelope |
capabilities.convert |
connect / disconnect |
materialize/dispose the client from a ConnectionConfig |
capabilities.live |
introspect |
read the live backend, then convert | capabilities.introspect |
query |
optional row read | capabilities.query |
subscribe / unsubscribe / cancel |
not declared โ rejected by the allow-list | capabilities.stream (false) |
@relationalize/node emits the richer canvas contract; the app consumes the
canonical DatabaseSchema from packages/schema/src/database.ts. src/convert/normalize.ts
is the single translation point:
relationalize database |
โ | canonical DatabaseSchema |
|---|---|---|
name |
โ | databaseName |
tables[].columns[].uiType ?? .type |
โ | columns[].type (free string) |
tables[].primaryKey[] |
โ | columns[].primaryKey |
tables[].foreignKeys[] |
โ | relationships[] |
tree, sql, source, generator |
โ | database.metadata.bridge (the doc's only escape hatch) |
Bridges must not invent top-level fields, so the rich artifacts ride inside
database.metadata โ the webview still only ever sees DatabaseSchema.
npm install # dev tooling only (typescript, tsdown, vitest)
npm run build # -> self-contained dist/ (no runtime deps)
npm test # 17 files / 326 tests (281 run, 45 live tests skipped)
npm run typecheckRun the bridge by hand:
echo '{"id":1,"method":"describe"}' | node bin/firebase-bridge.mjs --once
echo '{"id":1,"method":"convert","params":{"payload":{"users":{"u1":{"name":"A"}}}}}' \
| node bin/firebase-bridge.mjs --onceconnect / introspect take a host-provided ConnectionConfig:
{
"dialect": "firebase-rtdb", // or "firestore"
"connectionId": "my-conn",
"params": { "databaseURL": "https://my-proj-default-rtdb.firebaseio.com" },
"credential": { "kind": "service-account", "value": "{...service-account json...}" }
}| Credential kind | How it is used |
|---|---|
service-account |
RS256 JWT โ OAuth token (signed with node:crypto), for live projects |
bearer |
sent as Authorization: Bearer โฆ |
none + params.emulatorHost / endpoint |
Firebase emulator / self-hosted |
Firestore introspect reads params.collections when given; otherwise it
discovers root collections via documents:listCollectionIds. params.depth > 1
pulls sub-collections in as child tables. RTDB introspect reads params.path
(default /).
Credentials arrive over stdin only, are never persisted, never logged and never echoed back.
- Manifest validation before use, plus a protocol-version gate.
- Method allow-list derived from
capabilitiesโ undeclared verbs are refused. - Output validation โ unknown fields are stripped, invalid nodes are dropped
with a
BridgeWarning, never a crash (plain functions, nozod). - Size caps โ
outputLimits.maxNodes/maxBytestruncate and emit atruncatedwarning instead of hanging the host. - Fixed warning enum โ
partial | type-conflict | duplicate-key | truncated | unsupported.
npm test runs everything with Vitest:
| Suite | Files | Covers |
|---|---|---|
test/ (driver) |
manifest, normalize, convert, server, driver |
the bridge contract, the canonical adapter, the verbs, guardrails |
test/relationalize/ |
ported from relationalize/node/test |
the relationalization algorithm, Schema/DDL, the rtdb_bridge pipeline, the CLI |
test/fires2rest/ |
ported from fires2rest/tests |
value conversion, query building, RTDB URL/snapshot behaviour, client auth |
Two porting changes were needed:
- the upstream
relationalizesuites usenode:test; they are imported fromvitestinstead (thenode:assert/strictassertions are kept verbatim), andcli.test.tsdrives the vendoredmain()in-process instead of spawning a compiledcli.js. - the live
fires2restintegration suites keep theirdescribe.skipIfgate, so they skip unless a backend is available:
# emulator
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 npm run test:live
# or a real project
FIREBASE_PROJECT_ID=... FIREBASE_CLIENT_EMAIL=... FIREBASE_PRIVATE_KEY=... npm run test:liveinterop.test.ts (which compares against firebase-admin) is intentionally not
ported โ it would add an external dependency the driver deliberately does not have.
npm run build (tsdown) bundles everything into dist/, so the shipped bridge
is self-contained: copy dist/, bin/, bridge.json and any machine with
Node โฅ 20 can run it โ no node_modules, no registry access, no Python.
src/vendor/fires2rest and src/vendor/relationalize are the driver's own
copies of the two private projects (see src/vendor/README.md). The only
external dependency they had โ jose in fires2rest/auth.ts โ was recreated
with node:crypto, which is what makes the zero-dependency claim true.
โ {"id":1,"method":"describe"} โ {"id":1,"ok":true,"result":{"id":"firebase","dialects":["firebase-rtdb","firestore"], ...}} โ {"id":2,"method":"convert","params":{"payload":{"users":{"u1":{"name":"A"}}}}} โ {"id":2,"ok":true,"result":{"protocol":"dbchart.bridge","protocolVersion":1, "driver":{"id":"firebase","kind":"realtime", ...}, "database":{"databaseName":"rtdb_to_sql","tables":[...],"relationships":[...]}, "warnings":[]}} โ {"id":3,"method":"introspect","params":{"connection":{...},"path":"/"}} โ {"id":3,"ok":true,"result":{ ...envelope... }} โ {"id":4,"method":"subscribe"} โ {"id":4,"ok":false,"error":{"code":"METHOD_NOT_ALLOWED","message":"..."}}