Pyric
Navigate

pyric/database compatibility matrix

100% of the public API supported

59 of 59 public API

Status legend

Conforming — observable behavior matches Firebase, locked by a passing probe Diverged (documented) — intentional difference with a written reason

Public API

pyric/database mirrors the public firebase/database API in the Pyric sandbox. The rows below compare its observable behaviour with Firebase.

getDatabase(ctx) builds a sandbox-target Database; frozen ctx.auth baked in
unit:modular/sandbox-target.test.ts ("getDatabase(ctx) returns a tagged Database handle")
getDatabase(sandbox) builds a sandbox-live target; reads sandbox.currentUser per op
unit:modular/sandbox-target.test.ts ("reads sandbox.currentUser at op time, not at getDatabase time")
ref(db, path?) returns a path-tagged DatabaseReference; default is root
unit:modular/sandbox-target.test.ts ("ref(db) returns a root ref" + "ref(db, ...) returns a path ref")
child(ref, 'sub/path') composes paths; result inherits the parent's target
unit:modular/sandbox-target.test.ts ("child(ref, 'sub') composes paths")
ref.parent returns the parent ref; root.parent === null
unit:modular/sandbox-target.test.ts ("ref.parent returns the parent ref; root.parent is null")
ref.root returns the root ref of the same target
unit:modular/sandbox-target.test.ts ("ref.root returns the root ref")
get(ref) returns a DataSnapshot-shaped object with val(), exists(), key, child(), hasChildren(), size, and toJSON(); the modular Web surface does not expose numChildren().
get on an absent path resolves to { val: null, exists: false } (matches DataSnapshot.val() contract)
unit:modular/sandbox-target.test.ts ("reads return null for an absent path")
set(ref, value) replaces the value at the path
Sandbox aligned: unit:modular/sandbox-target.test.ts ("round-trips a primitive value" + "round-trips nested objects"); matches oracle observation packages/conformance/observations/rtdb/rtdb-set-then-get-roundtrip.json (prod observation blocked on rules; sandbox locks the contract directly)
set(ref, null) deletes the subtree at the path
Sandbox aligned: unit:modular/sandbox-target.test.ts ("set(ref, null) deletes the path"); matches oracle observation packages/conformance/observations/rtdb/rtdb-remove-vs-set-null.json
remove(ref) is equivalent to set(ref, null) (same end state)
Sandbox aligned: unit:modular/sandbox-target.test.ts ("remove and set(null) produce identical end-state"); matches oracle observation packages/conformance/observations/rtdb/rtdb-remove-vs-set-null.json
update(ref, patch) shallow-merges top-level keys at the ref's path
unit:modular/sandbox-target.test.ts ("shallow-merges top-level keys")
null value in a shallow update deletes that key
unit:modular/sandbox-target.test.ts ("null values in a shallow update delete the key")
update(rootRef, { '/a/x': v1, '/b/y': v2 }) is a multi-path atomic write — all paths land or none do
unit:upstream-rtdb-probes.test.ts ("one update nulls, mutates, and displaces within a limitToFirst window") + unit:modular/sandbox-target.test.ts (atomic multipath + rules denial); matches the matrix #23 prod contract
Overlapping multi-path updates (one path is a descendant of another) reject before any path is written
unit:modular/sandbox-target.test.ts ("rejects overlapping paths")
push(ref) mints a 20-char auto-id key starting with -, lexicographically sortable
Sandbox aligned: unit:modular/sandbox-target.test.ts ("mints 20-char keys starting with \"-\"" + "sequential push keys are lex-sortable"); matches oracle observation packages/conformance/observations/rtdb/rtdb-push-autoid-format.json
push(ref, value) writes value at the new child path
unit:modular/sandbox-target.test.ts ("push(ref, value) writes the value at the new child path")
pushKey() mints a fresh push-shaped key without writing — used by callers building multi-path updates that need the key first
unit:modular/sandbox-target.test.ts ("pushKey() mints a fresh key without writing")
serverTimestamp() returns the { ".sv": "timestamp" } sentinel marker the wire encoder recognises
Sandbox aligned: unit:modular/sandbox-target.test.ts ("serverTimestamp() returns the documented shape"); matches the prod wire contract
serverTimestamp() resolves to a number (epoch ms) on read-back
Sandbox aligned: unit:modular/sandbox-target.test.ts ("resolves to a number on read-back"); matches oracle observation packages/conformance/observations/rtdb/rtdb-servertimestamp-resolves.json (prod observation blocked on rules; sandbox locks the contract directly)
serverTimestamp() sentinels resolve when nested inside multi-path update payloads
unit:modular/sandbox-target.test.ts ("resolves sentinels nested deep inside an update payload")
Rules-denied write throws a plain Error (NOT a FirebaseError) with .code === 'PERMISSION_DENIED' (uppercase snake-case) and .message === 'PERMISSION_DENIED: Permission denied'
Sandbox aligned: unit:modular/sandbox-target.test.ts ("rules-denied set throws a plain Error with PERMISSION_DENIED code"); matches oracle observation packages/conformance/observations/rtdb/rtdb-rules-denied-error-code.json (against blockingfun, fb-js-sdk 12.13.0)
Rules-denied read throws the same plain-Error PERMISSION_DENIED shape as a denied write
Sandbox aligned: unit:modular/sandbox-target.test.ts ("rules-denied get throws the same plain Error shape"); matches oracle observation packages/conformance/observations/rtdb/rtdb-rules-denied-error-code.json
Rules-denied remove throws the same plain-Error PERMISSION_DENIED shape
Sandbox aligned: unit:modular/sandbox-target.test.ts ("rules-denied remove throws the same plain Error shape")
onValue(ref, cb) fires immediately on subscribe with the current value at the path
unit:modular/sandbox-target.test.ts ("fires on subscribe with the current value")
onValue fires again after every write that CHANGES the value at the watched path; a write that leaves the watched subtree byte-identical (a no-change re-write, or an ancestor/descendant write that doesn't alter this path) is suppressed (DB-B8)
unit:modular/sandbox-target.test.ts ("fires after every write that touches the watched path") + unit:modular/no-change-suppression.test.ts ("re-writing the same value does NOT re-fire" + "ancestor write leaving the subtree unchanged does NOT fire")
onValue fires after a descendant write (the listener sees subtree changes)
unit:modular/sandbox-target.test.ts ("fires on a descendant write")
onValue initial-fire for an absent path delivers val=null, exists=false (matches matrix expectation locked by oracle for sentinel/listener shape)
unit:modular/sandbox-target.test.ts ("absent path: initial fire delivers val=null, exists=false")
The onValue return value is an unsubscribe function; calling it stops further fires
unit:modular/sandbox-target.test.ts ("fires after every write that touches the watched path" — checks unsubscribed listener doesn't fire on subsequent write)
onChildAdded / onChildChanged / onChildRemoved / onChildMoved — plain-ref subscription surface
unit:modular/sandbox-child-events.test.ts exercises all four plain-reference child listener registrars; M41–M48 own their per-event oracle claims.
onChildAdded replays each existing direct child of the parent ref on subscribe (one fire per existing key)
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("replays existing direct children on subscribe — one fire per key"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-onchildadded-initial-replay.json (seeded {k1,k2,k3}, observed firedKeys: ['k1','k2','k3']).
After subscribe, onChildAdded fires exactly once per new direct child write; snapshot carries {key, val} of the new child
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("fires exactly once per NEW direct child after subscribe"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-onchildadded-post-subscribe.json (postSubscribeFires: 1, lastFire: {key:'k3', val:{v:3}}).
onChildChanged has NO initial replay; fires once when an existing direct child's value transitions; snapshot carries the NEW value
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("does NOT fire on subscribe (no initial replay)" + "fires once when an existing child transitions to a new value; snapshot carries NEW val"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-onchildchanged-fires-on-update.json (firedOnInitial: 0, firedOnUpdate: 1, lastFire: {key:'k1', val:{v:2}}).
onChildChanged does NOT fire for added or removed children — those go to the other event listeners
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("does NOT fire when a child is added" + "does NOT fire when a child is removed").
onChildRemoved has NO initial replay; fires once when a direct child is deleted (via remove(child) or set(child, null)); snapshot carries the PRIOR (now-removed) value
unit:upstream-rtdb-probes.test.ts (parent wipe fan-out via remove(parent) / set(parent, scalar)) + unit:modular/sandbox-child-events.test.ts (single-child delete carries PRIOR val); matches oracle rtdb-modular-onchildremoved-fires-on-delete.json
A plain-reference onChildMoved uses Firebase's default priority index: ordinary value changes that preserve priority do not move, while each changed child's priority emits one event even if its predecessor stays the same
Oracle rtdb-modular-priority-contract captures the same one-event-per-priority-operation sequence on a plain ref and an explicit orderByPriority() query, including a priority change that retains the same predecessor; unit:modular/sandbox-child-events.test.ts separately pins no movement for an ordinary value-only change. (Structured evidence: priority-metadata-cdd.test.ts)
off(ref) (no event type) removes ALL listeners at that ref — value + every child event variety
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("off(ref) removes ALL listeners at the ref" + "off(ref) also removes value listeners at the same path"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-off-stops-child-fires.json (postOffFires: 0).
off(ref, eventType?, callback?) variants: off(ref, 'value') / off(ref, 'child_added') / off(ref, eventType, cb) remove the targeted subset; returned-unsubscribe from onChild* is equivalent to off(ref, eventType, cb)
Sandbox aligned: unit:modular/sandbox-child-events.test.ts ("off(ref, \"child_added\") removes only that event variety" + "off(ref, \"value\") removes only value listeners" + "off(ref, eventType, cb) removes only the matching callback" + "returned-unsubscribe from onChildAdded is functionally equivalent to off()").
connectDatabaseEmulator(db, host, port) is a no-op on sandbox targets (the sandbox IS a local emulator)
unit:modular/sandbox-target.test.ts ("is a no-op on sandbox handles")
sandbox.setRules(db, rulesJson) deploys rules to the in-process simulator; setRules(db, null) clears rules (default-allow)
unit:modular/sandbox-target.test.ts ("sandbox.setRules(db, null) clears rules")
sandbox.setData(db, { '/path': value }) bulk-loads data, bypassing rules
unit:modular/sandbox-target.test.ts ("sandbox.setData seeds the tree (rule-bypass)")
sandbox.snapshotState(db) dumps the full tree as a plain JSON object
unit:modular/sandbox-target.test.ts ("sandbox.snapshotState dumps the full tree")
query(ref, ...constraints) + ordering/range constraints
unit:upstream-rtdb-probes.test.ts ("orderByChild('a/b') + limitToFirst orders by the nested path") + unit:modular/queries.test.ts + oracle observations under packages/conformance/observations/rtdb-modular/; see M49–M64 for the per-claim breakdown.
runTransaction(ref, fn, options?) resolves to { committed: boolean, snapshot: DataSnapshot } for the happy path — the update fn return value is written, committed is true, and snapshot.val() reflects the committed value
Sandbox aligned: unit:modular/transaction.test.ts ("resolves to { committed: boolean, snapshot } with the committed value"); matches oracle observations packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-success.json + rtdb-modular-runtransaction-returns-committed-snapshot.json (against blockingfun, fb-js-sdk 12.13.0) (Structured evidence: rtdb-modular-runtransaction-warm-client-speculation)
Returning undefined from the update fn ABORTS the transaction — resolves { committed: false, snapshot }; no write performed, no listener fan-out
Sandbox aligned: unit:modular/transaction.test.ts ("returning undefined aborts — committed: false, no write" + "aborted transaction does NOT fan out to listeners"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-abort-undefined.json — known divergence: prod's result.snapshot.val() reflects the CLIENT's pre-fetch (often null even when the server has a value because the speculative invocation runs before the server snap arrives); the sandbox returns the actual pre-transaction value at the path (more useful in single-client harness). The agreed-upon contract callers should rely on is committed === false and unchanged server-side data, NOT the snapshot's .val() on the abort path.
Cold production clients may invoke the update fn first with speculative null and then with the current seeded value; the always-warm in-process sandbox invokes it once with the current value
Oracle rtdb-modular-runtransaction-current-value-arg captures two seeded-path production invocations (null, then current); unit:modular/oracle-conformance-transactions.test.ts pins the sandbox's single current-value invocation. The separate warm-client observation matches the sandbox but does not erase the cold-client contract. (Structured evidence: transaction.test.ts)
The update fn arg is a defensive deep clone — mutating it does NOT corrupt the stored tree (matters for code that does current.count++; return undefined and expects abort to preserve state)
unit:modular/transaction.test.ts ("mutating the update-fn arg does NOT corrupt the stored tree") — no separate oracle row (defensive contract; prod behavior is identical because the SDK clones on the wire boundary)
options.applyLocally controls whether the in-flight optimistic value fans out to onValue listeners — default true (apply locally before commit); false suppresses intermediate fires so listeners see only the committed value
Sandbox aligned: unit:modular/transaction.test.ts ("applyLocally: true (default) — listener sees initial + committed value" + "applyLocally: false — listener sees only the committed value"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-options-applylocally.json (single-client harness: both branches produce 2 fires (initial + commit) — divergence vs prod's documented multi-client suppression would surface under contention, which the sandbox doesn't model)
Rules-denied transaction rejects with a plain Error whose message === 'permission_denied' (lowercase) and NO .code field — DIFFERENT from set/get's 'PERMISSION_DENIED: Permission denied' shape with uppercase .code.
Sandbox aligned: unit:modular/transaction.test.ts ("rejects with a plain Error whose message is \"permission_denied\""); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-on-rules-denied-path.json (against blockingfun: message: 'permission_denied', code: null, constructorName: 'Error').
Rules-denied transaction does NOT write — pre-transaction value at the path is preserved through the rejection
unit:modular/transaction.test.ts ("does not write to the path when rules deny") — locked alongside the M37e shape claim
Committed transaction fans out to onValue listeners on the watched path with the new value (default applyLocally behavior)
unit:modular/transaction.test.ts ("committed write fans out to onValue listeners")
Divergence: two ordinary concurrent runTransaction calls are serialized by the in-process backend, so their update functions are not retried with Firebase's captured contention counts. A synchronous re-entrant conflicting write does trigger a deterministic retry.
Oracle rtdb-modular-concurrent-transforms captures invocation counts [2, 3]; unit:modular/transaction-contention-cdd.test.ts pins the sandbox's [1, 1] ordinary-concurrency boundary and separately covers its deterministic re-entrant retry seam.
Identity-aware sandbox-live op routing — sign-in/sign-out via pyric/auth is observed by the next RTDB op without re-binding
unit:modular/sandbox-target.test.ts ("reads sandbox.currentUser at op time, not at getDatabase time")
Backend identity is per-Sandbox — two getDatabase(sandbox) calls on the same sandbox share data, two on different sandboxes don't
unit:modular/database-instances-cdd.test.ts proves same-sandbox sharing and independent-sandbox isolation.
Sandbox refs carry a stable key (last path segment) and toString() returning sandbox://rtdb/<path>
unit:modular/oracle-conformance-reference-writes.test.ts pins the stable key and local sandbox://rtdb/<path> string boundary alongside the production URL observation.
query(ref, orderByChild(p), startAt(v), endAt(w)) window is BOTH-INCLUSIVE — children whose ordered field === v or === w are included
unit:upstream-rtdb-probes.test.ts (deep orderByChild nested path) + unit:modular/queries.test.ts ("returns children whose ordered child is within [startAt, endAt] inclusive"); matches oracle rtdb-modular-orderbychild-window.json
orderByKey() orders children by RTDB nameCompare — integer-looking keys sort numerically FIRST (so ['1','2','10'], not the lexicographic ['1','10','2']), then non-integer keys lexicographically; startAt/endAt cursors + the optional key tie-breaker use the same order (DB-B4)
unit:upstream-rtdb-probes.test.ts (INT32 overflow/underflow cursors) + unit:modular/queries.test.ts + unit:modular/name-compare.test.ts; matches oracle rtdb-modular-orderbykey-window.json and upstream core/util/util.ts:253-276
Production rejects an unindexed orderByValue() + limitToFirst(N) query; the sandbox does not enforce .indexOn and returns the N smallest primitive values
Oracle rtdb-modular-orderbyvalue-numeric captures production's Index not defined rejection; unit:modular/oracle-conformance-queries.test.ts pins that rejection beside the sandbox's successful [10,20,30] window. (Structured evidence: oracle-conformance-queries.test.ts)
orderByChild(p) + equalTo(v) returns ALL children whose field at p === v — no uniqueness enforced
Sandbox aligned: unit:modular/queries.test.ts ("returns ALL children whose ordered field === the supplied value"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-equalTo-filter.json (both 'b'-grouped children returned).
equalTo with no matches returns an empty snapshot (exists() === false, size === 0)
unit:modular/queries.test.ts ("returns an empty snapshot when nothing matches")
limitToFirst(N) keeps the lowest-ranked N children (post-ordering, pre-filter)
Sandbox aligned: unit:modular/queries.test.ts ("limitToFirst takes the lowest-ranked window"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-limittofirst-vs-limittolast.json (firstPositions [1,2]).
limitToLast(N) keeps the highest-ranked N children
Sandbox aligned: unit:modular/queries.test.ts ("limitToLast takes the highest-ranked window"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-limittofirst-vs-limittolast.json (lastPositions [4,5]).
limitToFirst(N) larger than the result returns the full window (no padding, no throw)
unit:modular/queries.test.ts ("limitToFirst(N) larger than the result returns the full window")
startAfter(v) and endBefore(v) are EXCLUSIVE — the boundary value is dropped from the result
Sandbox aligned: unit:modular/queries.test.ts ("startAfter + endBefore drop the boundary values"); matches oracle observation packages/conformance/observations/rtdb-modular/rtdb-modular-startafter-endbefore-exclusive.json (positions [3,4], cursors 2 + 5 dropped).
onValue(query, cb) only fires when the windowed result changes — writes OUTSIDE the window don't re-fire the listener; writes that displace a member DO
unit:upstream-rtdb-probes.test.ts ("one update nulls, mutates, and displaces within a limitToFirst window") + unit:modular/queries.test.ts ("fires only when the windowed result changes"); matches oracle rtdb-modular-onvalue-with-query.json
onValue(query) initial fire delivers an empty window (size === 0) when the path is absent
unit:modular/queries.test.ts ("initial fire on an empty path delivers an empty window")
query(query(ref, c1), c2) composes constraints — chaining folds both into one spec
unit:modular/queries.test.ts ("query(query(ref, c1), c2) composes constraints")
Snapshot from a query exposes children via snap.forEach in the executor-computed order — NOT necessarily the order Object.entries(val) would yield
unit:modular/queries.test.ts ("forEach visits children in ascending order of the child key")
startAt(value, key) uses key as the tie-breaker when multiple children share the same ordered value — children before key are dropped, the row at key is included (inclusive cursor)
unit:modular/queries.test.ts ("startAt with key tie-breaker drops earlier same-value children")
orderByChild('p') on children missing the field treats their value as null (sorts FIRST per RTDB's type ordering)
unit:modular/queries.test.ts ("orderByChild on a missing child path treats those children as null")
query on a path holding a primitive (or absent path) returns an empty snapshot — no rows to iterate
unit:modular/queries.test.ts ("query on a path with primitive value returns no rows")
Write-boundary normalization (nodeFromJSON-equivalent): a value written as an array is stored as an integer-keyed object — child(ref, '1') returns the element, forEach iterates 0,1,2… (DB-B2)
Sandbox aligned: unit:modular/normalization.test.ts ("array write is addressable by integer-string child key" + "forEach over an array iterates its elements"); upstream core/snap/nodeFromJSON.ts:118-128, core/snap/ChildrenNode.ts:194-230
Read-side array coercion: a dense integer-keyed object renders back as an array on snap.val() (allIntegerKeys && maxKey < 2 * numKeys) (DB-B2)
Sandbox aligned: unit:modular/normalization.test.ts ("a dense integer-keyed object reads back as an array"); upstream core/snap/ChildrenNode.ts:196-230
null children and empty objects are pruned at the write boundary — set(ref, {}) is equivalent to remove(ref); nested null collapses empty ancestors ("empty nodes don't exist") (DB-B3)
Sandbox aligned: unit:modular/normalization.test.ts ("set(ref, {}) is equivalent to remove" + "null children are pruned"); upstream core/snap/nodeFromJSON.ts:78-88,122-126
Write validation: an undefined payload, a non-finite number (NaN/±Infinity), or a key containing a forbidden char (., #, $, /, [, ], control chars) is rejected with a plain Error (DB-B1)
Sandbox aligned: unit:modular/normalization.test.ts ("rejects an undefined payload" + "rejects an invalid key" + "rejects a non-finite number"); upstream core/util/validation.ts:45,58,112-199
Conflicting query constraints throw synchronously at query(...) construction (NOT silent last-win): multiple orderBy*, a second limitToFirst/limitToLast, a second start (startAt/startAfter/equalTo) or end (endAt/endBefore/equalTo) (DB-B5)
Sandbox aligned: unit:modular/constraint-conflicts.test.ts (5 cases); upstream api/Reference_impl.ts:160-165,1824-1841,1888-1905,1945-1951,2193-2206
push(ref, value?) returns a ThenableReference (a DatabaseReference with .then/.catch). The key + ref are minted CLIENT-SIDE and available synchronously even when the optional value write is rules-denied; the write is deferred onto the promise, so a denial REJECTS the awaited push rather than throwing synchronously and discarding the key (DB-B7)
Sandbox aligned: unit:modular/push-thenable.test.ts (4 cases); matches oracle packages/conformance/observations/rtdb/rtdb-push-autoid-format.json ("available immediately even when the subsequent server write is denied by rules") + upstream api/Reference_impl.ts:599-630
DataSnapshot shape: size (getter), priority (null when absent), exportVal(), key, ref, val(), exists(), child(), hasChild(), hasChildren(), forEach(), toJSON(). It does NOT ship the legacy namespaced numChildren() method (DB-B10)
Sandbox aligned: unit:modular/snapshot-shape.test.ts ("exposes size/priority/exportVal; NOT numChildren()"); matches oracle packages/conformance/observations/rtdb-modular/rtdb-modular-get-snapshot-shape.json (hasSize: true, hasNumChildren: false) + upstream api/Reference_impl.ts:288-447. Flipped masking tests: modular/queries.test.ts + modular/sandbox-target.test.ts asserted snap.numChildren() — updated to snap.size.
Object-valued children are ORDER-EQUAL — the sort/range tie is broken by key (nameCompare), NOT by an invented JSON.stringify ordering; a query re-write that only reorders object keys is "no change" and doesn't re-fire (DB-B11)
Sandbox aligned: unit:modular/object-order-equality.test.ts; upstream core/snap/ChildrenNode.ts:386-400
A primitive at the ROOT is legal (set(ref(db), 'hello')); a subsequent child write replaces the primitive root ("writes win") (DB-B13)
Sandbox aligned: unit:modular/root-primitive.test.ts (2 cases)
onValue(ref, cb, { onlyOnce: true }) fires once then auto-unsubscribes (DB-B12)
Sandbox aligned: unit:modular/onvalue-onlyonce.test.ts; upstream api/Reference_impl.ts:975-980
Child-listener callbacks receive Firebase's previousChildName second argument, including initial replay and ordered-query movement.
Oracle rtdb-modular-child-previous-name captures initial replay plus add/change/remove/move predecessor values; replayed by unit:modular/listener-lifecycle-cdd.test.ts.
Value and child listener overloads accept a cancellation callback and deliver Firebase-shaped permission errors when a listen is denied or revoked; a callbackless listener that loses auth access is terminal and does not resurrect after sign-in.
Oracle rtdb-modular-listener-cancellation captures all five registrars returning normally, asynchronous initial-denial cancellation, one revocation callback each, and the Error/PERMISSION_DENIED/message shape; replayed in unit:modular/listener-lifecycle-cdd.test.ts.
All child-listener functions accept Query inputs; add/change/remove events respect the active query window.
unit:modular/listener-lifecycle-cdd.test.ts and unit:modular/sandbox-child-events.test.ts cover initial windows plus entering, leaving, and changing members.
onChildMoved fires when an ordered child changes position and co-fires with child_changed when Firebase does, preserving the captured previousChildName sequence.
Oracle rtdb-modular-child-previous-name and the modular oracle suite replay ordered movement, co-fire behavior, and predecessor sequencing. (Structured evidence: rtdb-modular-childchanged-cofire-with-childmoved, rtdb-modular-onchildmoved-previouschildname-sequencing, listener-lifecycle-cdd.test.ts, oracle-conformance-listeners.test.ts)
Child listener overloads accept ListenOptions directly or after a cancellation callback; onlyOnce stops delivery after Firebase's captured initial/event batch.
Oracle rtdb-modular-child-listener-only-once captures all four listener families, both overload shapes, the existing-child replay batch, and one-shot post-registration delivery; replayed by unit:modular/listener-lifecycle-cdd.test.ts.
.validate rules are enforced on modular sandbox writes through set, atomic update, and runTransaction; a descendant validation failure rejects the operation without changing state.
unit:modular/sandbox-target.test.ts executes all three write paths against a required-child .validate rule and proves each rejects without committing state. The shared SimulateHandler behavior is production-locked by RTDB rules corpus rows #4 and #15.
onDisconnect(ref) returns the Firebase-shaped five-method handle; registration methods and cancel() return acknowledged Promises
An acknowledged onDisconnect(ref).set(value) registration does not mutate server data before the client disconnects
Oracle rtdb-modular-ondisconnect-registration; sandbox registration/no-mutation test. (Structured evidence: on-disconnect.test.ts)
goOffline(db) drains that client's queue once, listeners observe the disconnect write in server order, and goOnline(db) does not resurrect executed operations
Oracle rtdb-modular-ondisconnect-clean-set; sandbox clean lifecycle test. (Structured evidence: on-disconnect.test.ts)
Disconnect queues support set, update, remove, exact cancellation, parent cancellation of queued descendants, and captured parent-set/child-set/child-cancel coalescing that preserves the existing canceled child
Oracle rtdb-modular-ondisconnect-operations-cancel; sandbox operation/cancellation test, including the captured parent-set + child-cancel merge result. (Structured evidence: on-disconnect.test.ts)
Rules are evaluated when a disconnect operation is registered and re-evaluated when it executes; an execution-time denial leaves server state unchanged
Oracle rtdb-modular-ondisconnect-rules with successful normal-write controls in both rule phases; sandbox rules-timing test. (Structured evidence: on-disconnect.test.ts)
Local lifecycle contract: disconnect queues are owned per Database client, drain on app deletion, clear without executing on sandbox reset, and are excluded from persisted RTDB snapshots
The explicit rtdb-modular#M82 assertion set in unit:database/on-disconnect.test.ts covers independent clients, app deletion, reset clearing, and snapshot exclusion; these sandbox-owned boundaries have no production oracle equivalent.
OnDisconnect.setWithPriority(value, priority) writes both the value and RTDB priority metadata when the disconnect queue drains.
Oracle rtdb-modular-ondisconnect-operations-cancel captures export-format priority 7; sandbox unit:database/on-disconnect.test.ts replays the value and priority.
Divergence (pending fix): clean goOffline, app deletion, playground pagehide, and best-effort MessagePort close drain queued operations, but unannounced total renderer/process loss is not guaranteed by the in-memory sandbox
Oracle rtdb-modular-ondisconnect-abrupt-exit proves Firebase executes an acknowledged registration after forced writer termination. The two-port worker integration exercises goOffline, served-app deletion, and non-persisted pagehide; browser MessagePort close delivery cannot prove total-process loss without durable host-owned leases. (Structured evidence: rtdb-integration.test.ts)
Database is exported as a runtime constructor value and handles returned by getDatabase() satisfy Firebase's observable prototype and instanceof contract.
Oracle rtdb-modular-runtime-class-identity; sandbox row assertion rtdb-modular#M85 replays runtime identity. (Structured evidence: oracle-conformance-runtime-identity.test.ts)
DataSnapshot is exported as a runtime constructor value and snapshots returned by reads/listeners satisfy Firebase's observable prototype and instanceof contract.
Oracle rtdb-modular-runtime-class-identity; sandbox row assertion rtdb-modular#M86 replays runtime identity. (Structured evidence: oracle-conformance-runtime-identity.test.ts)
QueryConstraint is exported as a runtime constructor value and constraints returned by orderBy*, bound, and limit factories satisfy Firebase's observable prototype and instanceof contract.
Oracle rtdb-modular-runtime-class-identity; concrete constraint subclasses satisfy instanceof QueryConstraint without using the base prototype directly, replayed by row assertion rtdb-modular#M87. (Structured evidence: oracle-conformance-runtime-identity.test.ts)
TransactionResult is exported as a runtime constructor value and transaction results satisfy Firebase's observable prototype, instanceof, and toJSON() contract.
Oracle rtdb-modular-runtime-class-identity; sandbox row assertion rtdb-modular#M88 replays runtime identity and toJSON(). (Structured evidence: oracle-conformance-runtime-identity.test.ts)
setPriority and setWithPriority store, replace, preserve, and clear valid RTDB priority metadata; parent exportVal() and toJSON() recursively include descendant priority metadata.
Oracle rtdb-modular-priority-contract captures round-trip/export shape, update/transaction preservation, plain-set clearing, and explicit clearing; replayed by unit:modular/priority-metadata-cdd.test.ts.
orderByPriority orders by Firebase priority with key tie-breaking and composes with bounds, equality, and limits; plain snapshots and unconstrained queries use the same default priority index, and invalid priority bounds throw synchronously.
Oracle rtdb-modular-priority-contract captures priority order, key tie-breaking, bounds, and limits; replayed by unit:modular/priority-metadata-cdd.test.ts.
Priority changes participate in plain and explicitly ordered child movement/listener sequencing, transactions, and updates without losing metadata.
Oracle rtdb-modular-priority-contract captures priority movement, update/transaction preservation, and descendant delivery when ancestor replacement clears metadata; replayed by unit:modular/priority-metadata-cdd.test.ts and unit:modular/priority-listeners.test.ts. Disconnect priority execution is owned separately by M83.
off(query) removes listeners only from the equivalent constrained query view, while off(ref) removes listeners from every query view at that path.
Oracle rtdb-modular-off-duplicate-registration captures exact constrained-query removal and all-view reference removal with independent live-listener controls; replayed by unit:modular/sandbox-child-events.test.ts.
Divergence: DatabaseReference and Query equality includes database target, path, and canonical query parameters like Firebase, but toJSON() serializes the local sandbox://rtdb/... identity instead of Firebase's HTTPS database URL.
Oracle rtdb-modular-reference-shape-url captures conforming equality plus HTTPS JSON serialization; unit:modular/oracle-conformance-reference-writes.test.ts pins both the production HTTPS shape and the sandbox's sandbox:// boundary.
Query constraint construction preserves Firebase's synchronous validation for limits, child paths, cursor keys, undefined/non-finite/object endpoint values, and index-specific endpoint compatibility, including the captured +Infinity and server-value priority acceptances
Oracle rtdb-modular-query-construction-validation repeats the complete validation matrix twice per run and matched across two clean production runs; unit:modular/query-validation.test.ts replays every captured field exactly.

getDatabase(target) — initializer

getDatabase(ctx) returns a tagged sandbox-target handle (frozen identity)
unit:modular/database-instances-cdd.test.ts asserts the returned Database carries a sandbox target.
getDatabase(sandbox) returns a tagged sandbox-live handle (per-op identity)
unit:modular/database-instances-cdd.test.ts asserts the returned Database carries a sandbox-live target.
Inactive canonical firebase/database imports remain the upstream package; the mirror does not create tagged production targets
Package-resolution boundary assertion in unit:modular/database-instances-cdd.test.ts proves a real Firebase Database remains untagged and is rejected by the mirror.
getDatabase() (no argument) — wrapped in the playground preview to supply the sandbox; a raw mirror call rejects with package-resolution guidance
Phase 3 Tier 5: virtualized in the playground preview scope. Wired at packages/playground/src/components/AppPreview.tsx (slot install with bare-call wrap), packages/playground/src/lib/preview/virtual-imports-plugin.ts (alias map), and packages/playground/src/lib/preview/preview-scope.ts (type-level slot). Mirrors the getAuth / getFirestore wrap pattern. Demo fixture: packages/playground/scripts/fixtures/rtdb-set-get-roundtrip.tsx (bare getDatabase() + set/get/remove round-trip with anonymous sign-in) passes end-to-end through the bun run debug:fixtures Playwright suite. (Structured evidence: lifecycle-and-identity.test.ts)
(wrap, fixture passing)
Two getDatabase(sandbox) calls share state (same underlying LocalEnvironment)
unit:modular/database-instances-cdd.test.ts writes through one handle and reads through another handle owned by the same sandbox.
Handle dispatch by TARGET_SYMBOL brand — refs route to their owning target via a refToTarget WeakMap (mirror of firestore's pattern)
unit:modular/database-instances-cdd.test.ts proves references from independent sandbox targets route to independent backends.

ref(db, path) / child / parent / root

ref(db, path) returns a tagged DatabaseReference carrying key, parent, root, and toString(), and synchronously rejects paths containing ., #, $, [, or ] with Firebase's captured error shape
Oracle rtdb-modular-reference-shape-url; sandbox row assertion rtdb-modular#100 replays navigation and every forbidden path character. (Structured evidence: oracle-conformance-reference-writes.test.ts)
ref(db) with no path returns the root ref (key === null, parent === null)
Oracle rtdb-modular-reference-shape-url; sandbox row assertion rtdb-modular#101. (Structured evidence: oracle-conformance-reference-writes.test.ts)
child(ref, 'a/b') joins a non-empty relative path, including embedded slashes, and synchronously rejects empty or forbidden-character paths with Firebase's captured error shape
Oracle rtdb-modular-reference-shape-url; sandbox row assertion rtdb-modular#102 replays embedded-path joining plus empty/invalid validation. (Structured evidence: oracle-conformance-reference-writes.test.ts)
ref.parent is null at root, otherwise the parent ref
Oracle rtdb-modular-reference-shape-url; sandbox row assertion rtdb-modular#103. (Structured evidence: oracle-conformance-reference-writes.test.ts)
ref.key is the final path segment, null for root
Oracle rtdb-modular-reference-shape-url; sandbox row assertion rtdb-modular#104. (Structured evidence: oracle-conformance-reference-writes.test.ts)
Unknown ref (not produced by this package) → TypeError in shim ops
Oracle rtdb-modular-reference-shape-url captures a synchronous TypeError; sandbox row assertion rtdb-modular#105 matches its timing and constructor. (Structured evidence: oracle-conformance-reference-writes.test.ts)

get(ref) — single read

Returns a DataSnapshot carrying .val(), .exists(), .key, .ref, .size (getter, returns child count), .hasChildren(), .hasChild(path), .forEach(cb). The legacy namespaced-SDK method .numChildren() is NOT on the modular DataSnapshot — use .size instead. Observed: hasNumChildren: false, size: 3 for a {a,b,c} object, forEachKeys: ['a','b','c'] against blockingfun, fb-js-sdk 12.13.0.
snap.val() returns null for a missing path (NOT a thrown error — RTDB diverges from Firestore here; getDoc returns exists()===false but get on RTDB just returns a null-val snapshot)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-get-missing-path.json — observed threw: false, val: null, exists: false on a never-written path against blockingfun. (Structured evidence: lifecycle-and-identity.test.ts)
snap.exists() is false when val() === null, true otherwise
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-get-missing-path.json — observed exists: false for val: null. (Structured evidence: lifecycle-and-identity.test.ts)
Round-trip: set(ref, payload) then get(ref) returns the payload (lock the basic write→read invariant)
oracle: packages/conformance/observations/rtdb/rtdb-set-then-get-roundtrip.json — the payload round-trips structurally on both sides (this row's claim holds). NOTE — adjacent divergence pinned in oracle-conformance.test.ts: prod returns object children in LEXICOGRAPHIC key order (the capture's roundTripEqual: false — a JSON.stringify round-trip against a non-sorted payload fails), while the sandbox preserves insertion order (stringify round-trip succeeds). Key-order-sensitive consumers behave differently.
Rules-denied read throws a plain Error (NOT a FirebaseError) with code: 'PERMISSION_DENIED' (uppercase snake-case) — matches the agent-tool rows #15/#20

set(ref, value) — full write

Replaces the value at the path entirely; resolves to undefined (unlike setDoc which resolves to void, RTDB's set is documented as Promise<void>)
Oracle rtdb-modular-write-return-validation captures null in JSON for the resolved undefined value; sandbox row assertion rtdb-modular#111 matches it. (Structured evidence: oracle-conformance-reference-writes.test.ts)
set(ref, null) removes the path entirely — equivalent to remove(ref), subsequent get returns null-val snapshot
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-set-null-equals-remove.json — observed beforeExists: true → afterExists: false, afterVal: null after set(ref, null). (Structured evidence: public-operations.test.ts)
Nested objects overwrite — set(ref, {a: 1}) after set(ref, {a: 1, b: 2}) leaves {a: 1} only, NOT a merge (RTDB set is replacement, not merge)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-set-replaces-not-merges.json — observed final: {a: 1} with b absent after the second set. (Structured evidence: public-operations.test.ts)
Primitive round-trip — numbers, strings, booleans, arrays all survive a set→get cycle
oracle: packages/conformance/observations/rtdb/rtdb-set-then-get-roundtrip.json — the payload round-trips structurally on both sides (this row's claim holds). NOTE — adjacent divergence pinned in oracle-conformance.test.ts: prod returns object children in LEXICOGRAPHIC key order (the capture's roundTripEqual: false — a JSON.stringify round-trip against a non-sorted payload fails), while the sandbox preserves insertion order (stringify round-trip succeeds). Key-order-sensitive consumers behave differently.
Rules-denied write throws plain Error with code: 'PERMISSION_DENIED' (same shape as #110)

update(ref, values) — partial / multi-path update

update(ref, {a: 1, b: 2}) merges top-level keys at the ref; unspecified keys preserved (in contrast to set's replacement)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-update-merges-keys.json — after set({a:1,b:2}) then update({a:10}), observed final: {a:10, b:2}. (Structured evidence: public-operations.test.ts)
Multi-path update — update(parentRef, { 'a/x': 1, 'b/y': 2 }) lands BOTH writes atomically at distinct subtrees (RTDB's most distinctive feature; this is the "fan-out" pattern)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-update-multipath-atomic.json — observed aX: 1, bY: 2 both readable after a single update call. (Structured evidence: public-operations.test.ts)
Multi-path update is atomic: if any path is denied by rules, the entire update rejects and no path is written
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-update-multipath-rules-denial.json — observed threw: true, code: 'PERMISSION_DENIED' AND okPathWrittenDespiteDenial: false (the otherwise-permitted path also rolled back). (Structured evidence: public-operations.test.ts)
Setting a key to null inside update removes that key — same equivalence as set(ref, null)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-update-null-removes-key.json — after set({a:1,b:2}) then update({a:null}), observed final: {b:2} with a absent. (Structured evidence: public-operations.test.ts)
Update path validation — overlapping paths (e.g. '/a' and '/a/x' in the same call) throws synchronously before any write
Oracle rtdb-modular-write-return-validation captures a synchronous ancestor-path error and unchanged terminal state; sandbox row assertion rtdb-modular#120 matches both. (Structured evidence: oracle-conformance-reference-writes.test.ts)

remove(ref) — delete

Removes the value AND all children; subsequent get returns null-val snapshot
Idempotent — remove on a path that's already absent resolves successfully (no-throw)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-remove-idempotent.jsonremove on a never-written path observed threw: false, afterExists: false. (Structured evidence: public-operations.test.ts)
remove(ref) and set(ref, null) produce the same end state — locks the documented RTDB invariant

push(ref, value?) — auto-id append

push(ref).key is a 20-char string starting with -, available synchronously (client-side mint, no server round-trip required)
Sequential push calls produce monotonically-sortable keys (timestamp-prefixed for chronological ordering via orderByKey)
push(ref, value) writes the value AND returns the new child ref (both behaviors in one call); push(ref) mints the ref without writing
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-push-with-value.jsonawait push(parent, {hello:'world'}) returned a ref with a 20-char key; subsequent get(r) returned {hello:'world'}. (Structured evidence: public-operations.test.ts)
The returned ref r = push(parent, value) is usable in follow-up ops: get(r), set(r, …), remove(r)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-push-with-value.json — observed all 4 follow-up ops succeed through the returned ref (refIsUsableForFollowupOps: true). (Structured evidence: public-operations.test.ts)

onValue(ref, cb) — value-level listener

Subscribing to a path with existing data fires the listener once with the current snapshot (the "initial fire")
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onvalue-initial-with-data.json — observed exactly 1 initial fire within ~46ms of subscribe, snapshot.val() === the seeded payload. (Structured evidence: public-operations.test.ts)
Subscribing to a nonexistent path still fires the listener once — with a null-val snapshot AND exists() === false. Matches Firestore's onSnapshot-on-missing-doc semantics: prod RTDB does NOT silently skip the initial fire for empty paths.
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onvalue-initial-no-data.json — observed 1 initial fire on a never-written path with firstFire.val: null, firstFire.exists: false (~55ms after subscribe). (Structured evidence: public-operations.test.ts)
Subsequent set(ref, …) fires the listener with the new value
oracle: packages/conformance/observations/rtdb/rtdb-onvalue-fires-on-set.json — observed 1 fire per set() (1+1+1 = 3 total: initial-null, after-first-set, after-second-set). (Structured evidence: public-operations.test.ts)
Unsubscribe — the returned unsubscribe function stops further fires; subsequent writes produce 0 additional fires after unsub()
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onvalue-unsubscribe.json — observed preUnsubFires: 2, postUnsubFires: 2 (a write performed after unsub() produced 0 additional fires within a 500ms settle window). (Structured evidence: public-operations.test.ts)
The returned value from onValue(ref, cb) is the unsubscribe function (NOT an object); calling it removes the listener
unit:modular/listener-lifecycle-cdd.test.ts asserts the return is a function and that invoking it stops subsequent delivery; production behavior is adjacent to oracle-backed row #131.

onChildAdded / onChildChanged / onChildRemoved / onChildMoved

onChildAdded replays the existing children on subscribe — one fire per existing child key, in the default priority index with key tie-breaking (unlike onValue which fires once with the parent snapshot)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onchildadded-initial-replay.json — seeded {k1, k2, k3}, observed 3 initial fires with firedKeys: ['k1', 'k2', 'k3']; the default priority/key order is separately pinned by rtdb-modular-priority-contract. (Structured evidence: public-operations.test.ts)
After subscribe, adding a child via push or set(child, …) fires onChildAdded exactly once for that key
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onchildadded-post-subscribe.json — seeded {k1,k2}, observed postSubscribeFires: 1, lastFire: {key:'k3', val:{v:3}} after writing the new child. (Structured evidence: public-operations.test.ts)
onChildChanged fires when an existing child's value changes; does NOT fire for added or removed children
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onchildchanged-fires-on-update.json — observed firedOnInitial: 0, firedOnUpdate: 1, lastFire: {key:'k1', val:{v:2}} (the NEW value, not the prior). (Structured evidence: public-operations.test.ts)
onChildRemoved fires when a child is deleted (via remove(child) or set(child, null)); snapshot carries the PRIOR value
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onchildremoved-fires-on-delete.json — observed firedOnDelete: 1, removedSnapCarriesPriorValue: true (snapshot.val() was the pre-delete value). (Structured evidence: public-operations.test.ts)
onChildMoved under an explicit ordered query fires when the ordered field changes, co-fires with child_changed, and supplies the captured previousChildName.

off(ref, eventType?, callback?) — unsubscribe variants

off(ref) removes ALL listeners at that ref (any event type, any callback)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-off-stops-child-fires.json — after off(ref) with no eventType, a subsequent write produced postOffFires: 0 against an onChildAdded registration. (Structured evidence: public-operations.test.ts)
off(ref, 'value') removes only value listeners at that ref
Sandbox aligned (M48); oracle: packages/conformance/observations/rtdb/rtdb-off-eventtype-precision.json — registered TWO value listeners + one child_added at the same ref; after off(ref, 'value') (no callback), valueListenersStopped: true (neither value cb fired on subsequent writes) AND childListenerStillFiringAfterOffValue: true (the child listener kept firing). offValueClearsAllValueListeners: true confirms the no-callback variant removes ALL value listeners at the ref. (Structured evidence: rtdb-modular-off-stops-child-fires, public-operations.test.ts)
off(ref, 'value', cb) removes only the specific callback
Sandbox aligned (M48); adjacent to #141 — the upstream off with the cb argument removes only the matching callback. Same probe (rtdb-onvalue-unsub-equivalence.json Case 2) confirms off(ref, 'value', cb) stops only that callback. (Structured evidence: oracle-conformance.test.ts)
The returned unsubscribe function from onValue(ref, cb) is equivalent to off(ref, 'value', cb)
Sandbox aligned (M48); oracle: packages/conformance/observations/rtdb/rtdb-onvalue-unsub-equivalence.jsonunsubReturnType: 'function', unsubReturnedFnStopsListener: true (the captured return value halted fires on write), offRefValueCbStopsListener: true (the same effect via off(ref, 'value', cb)), bothFormsEquivalent: true. (Structured evidence: public-operations.test.ts)
off(ref, eventType, callback)When the same callback is registered more than once, each off(ref, eventType, callback) removes one registration without orphaning the others
Oracle rtdb-modular-off-duplicate-registration proves each matching off removes one registration; sandbox row assertion rtdb-modular#183 replays the full sequence. (Structured evidence: oracle-conformance-reference-writes.test.ts)

query(ref, ...constraints) + ordering / bounds / limits

query(ref, orderByChild('field'), limitToFirst(N)) returns a Query whose get() resolves a snapshot containing N children ordered by field
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-query-orderbychild-limit.json — seeded 4 children with positions [3,1,4,2], observed orderedKeys: [{key:'a',pos:1}, {key:'b',pos:2}] (first 2 in ascending order). Requires .indexOn declared in rules. (Structured evidence: rtdb-modular-orderbychild-window, public-operations.test.ts)
orderByKey() orders by the auto-id / numeric key
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-orderbykey-window.json — seeded {a,b,c,d,e} in shuffled insertion order, observed matchedKeys: ['b','c','d'] for orderByKey() + startAt('b') + endAt('d') (in key order). (Structured evidence: public-operations.test.ts)
Production enforces .indexOn: '.value' for orderByValue() while the sandbox executes the value ordering without index enforcement
Oracle rtdb-modular-orderbyvalue-numeric rejects the unindexed production query; unit:modular/oracle-conformance-queries.test.ts pins the sandbox's successful ordered result instead of treating that contradiction as conformance.
equalTo(v) filters children whose ordered field === v (returns 0, 1, or multiple matches — RTDB does NOT enforce uniqueness)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-query-equalto.json — seeded {red, blue, blue, green}, observed matchedKeys: ['k2', 'k3'] for equalTo('blue') (both blue children, none of the others). Additional probe: packages/conformance/observations/rtdb-modular/rtdb-modular-equalTo-filter.json (a..b..c groups) confirms equalTo('b') returns the two b children. (Structured evidence: public-operations.test.ts)
startAt(v) is inclusive (the child whose ordered value === v is included)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-query-startat-inclusive.json — seeded positions [1,2,3,4], observed matched: [2,3,4] for startAt(2) (cursor doc included). (Structured evidence: rtdb-modular-orderbychild-window, rtdb-modular-orderbykey-window, public-operations.test.ts)
endAt(v) is inclusive
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-orderbychild-window.jsonstartAt(2) + endAt(4) matched positions [2,3,4] (endAt(4) included its boundary value). (Structured evidence: rtdb-modular-orderbykey-window, public-operations.test.ts)
startAfter(v) is exclusive
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-startafter-endbefore-exclusive.jsonstartAfter(2) + endBefore(5) matched positions [3,4] (cursor 2 dropped). (Structured evidence: public-operations.test.ts)
endBefore(v) is exclusive
limitToFirst(N) caps the result count from the start of the ordered range
limitToLast(N) caps from the end
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-limittofirst-vs-limittolast.json — observed lastKeys: ['d','e'], lastPositions: [4,5] for limitToLast(2) on a 5-child collection ordered by pos. (Structured evidence: public-operations.test.ts)
Listeners on a Query (onValue(q, …)) emit only the windowed snapshot — NOT the parent ref's full data
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-onvalue-with-query.json — seeded 3 children, watched first 2 by pos; observed 3 fires total: (1) initial [a,b], (2) OUTSIDE-window write to c/extra did NOT fire, (3) INSIDE-window mutation of a re-fired, (4) new child z displaced b and re-fired. Outside-window writes are silent. (Structured evidence: public-operations.test.ts)

Sentinels — serverTimestamp() / increment(n)

serverTimestamp() resolves server-side to a number (epoch milliseconds) — diverges from Firestore's Timestamp instance
oracle: packages/conformance/observations/rtdb/rtdb-servertimestamp-resolves.json — observed createdAtType: 'number', createdAt: 1779075391118 (i.e. a plain JS number, NOT a Timestamp object). (Structured evidence: public-operations.test.ts)
serverTimestamp() as a field value in set or update writes the {".sv": "timestamp"} sentinel; the read-back value is the resolved number
oracle: packages/conformance/observations/rtdb/rtdb-servertimestamp-resolves.json — read-back showed createdAtSentinelShape: false (sentinel resolved server-side; client sees the number, not the .sv placeholder). (Structured evidence: public-operations.test.ts)
increment(n) against a missing field starts at 0 (so increment(5) lands as 5)
Sandbox aligned (modular increment export now present): unit:modular/increment.test.ts ("increment against a missing field starts from 0"); matches oracle packages/conformance/observations/rtdb-modular/rtdb-modular-increment-from-missing.json — observed afterFirst: 5 from increment(5) against an absent count field.
increment(n) against an existing numeric field adds atomically; negative deltas subtract
Sandbox aligned: unit:modular/increment.test.ts ("subsequent increments accumulate (positive then negative)" + "nested inside an update patch resolves per-field"); matches oracle packages/conformance/observations/rtdb-modular/rtdb-modular-increment-from-missing.json — observed afterSecond: 8 (5+3) then afterNegative: 6 (8-2).
Divergence: production interleaves concurrent increment writes from independent clients and preserves both deltas; the synchronous in-process backend applies each call before returning its promise, so a Promise.all call site is observably serialized even though its terminal value also reaches 5
Oracle rtdb-modular-concurrent-transforms uses two independent clients and captures terminal 5 after concurrent +2/+3 increments; unit:modular/transaction-contention-cdd.test.ts pins the sandbox's synchronous first-write visibility and matching terminal value without claiming that terminal equality proves contention semantics.

runTransaction(ref, transactionUpdate, options?) — optimistic concurrency

Basic success — runTransaction(ref, current => (current ?? 0) + 1) resolves { committed: true, snapshot } where snapshot.val() is the new value
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-success.json — observed committed: true, snapVal: 1 after running current => (current ?? 0) + 1 against an empty ref. (Structured evidence: rtdb-modular-runtransaction-on-rules-denied-path, rtdb-modular-runtransaction-options-applylocally, public-operations.test.ts)
Returning undefined from the update fn aborts the transaction — resolves { committed: false }, no write performed (RTDB-specific; distinct from Firestore where the only abort path is throwing)
oracle: packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-abort-undefined.json — seeded 100 then transaction returned undefined; observed committed: false, snapVal: null, afterValOnServer: 100 (existing value preserved). (Structured evidence: public-operations.test.ts)
A cold production client can call the update fn with speculative null before the current seeded value; the always-warm sandbox calls it once with the current value
Oracle rtdb-modular-runtransaction-current-value-arg captures the cold two-call sequence and rtdb-modular-runtransaction-warm-client-speculation captures the matching warm one-call case; unit:modular/oracle-conformance-transactions.test.ts pins both sides. (Structured evidence: rtdb-modular-runtransaction-options-applylocally, rtdb-modular-runtransaction-success)
Concurrent contention — if another client writes between the read and write, the update fn is retried with the new current value (typically up to 25 retries by default)
Oracle rtdb-modular-concurrent-transforms captures callback counts [2, 3] across two clients; the synchronous in-process backend serializes the same ordinary calls with [1, 1], pinned by unit:modular/transaction-contention-cdd.test.ts.
Result snapshot's .val() reflects the committed value (or the existing value if aborted)

goOnline / goOffline — connection control

goOffline(db) drains that client's onDisconnect queue once, but does not make the in-memory data plane unreachable; ordinary writes, listeners, and get() remain available
logical disconnect lifecycle is modeled, but the in-memory data plane itself remains available
goOnline(db) reconnects the logical lifecycle without resurrecting drained operations; it is otherwise a no-op because the in-memory data plane never became unreachable
logical disconnect lifecycle is modeled, but the in-memory data plane itself remains available

Transport, logging, and URL-reference exports

forceLongPolling() — accepted no-op: transport selection is not applicable to the in-process/worker sandbox (it never opens a real socket). Accepted so init code that calls it compiles + runs
transport selection not applicable to the in-process/worker sandbox
forceWebSockets() — accepted no-op: transport selection is not applicable to the in-process/worker sandbox (see forceLongPolling)
transport selection not applicable to the in-process/worker sandbox
enableLogging(logger?, persistent?) — accepted no-op: the sandbox has no modular-SDK-style logger to wire a level/sink into (it uses host-level console logging directly, matching pyric/firestore's setLogLevel). Accepted so init code that calls it compiles + runs
accepted no-op; no sandbox logger to wire into
Divergence: refFromURL(db, url) matches Firebase's path parsing, query ignoring, FTP-to-database normalization, and fragment rejection, but the single-database sandbox does not validate the URL host/namespace against the handle; Firebase rejects a mismatched host while the sandbox uses its path.
Oracle rtdb-modular-reference-shape-url captures accepted FTP/query URLs and rejected fragments, malformed URLs, and host mismatches; unit:modular/oracle-conformance-reference-writes.test.ts pins the conforming validations plus both sides of the host divergence.
path and validation behavior conform except URL host/namespace matching (single-database sandbox)

Current gaps

Documented divergences

Known differences between Pyric and production Firebase. Each remains tracked as a non-conforming row.

Cold production clients may invoke the update fn first with speculative null and then with the current seeded value; the always-warm in-process sandbox invokes it once with the current value
Oracle rtdb-modular-runtransaction-current-value-arg captures two seeded-path production invocations (null, then current); unit:modular/oracle-conformance-transactions.test.ts pins the sandbox's single current-value invocation. The separate warm-client observation matches the sandbox but does not erase the cold-client contract. (Structured evidence: transaction.test.ts)
**Divergence:** two ordinary concurrent runTransaction calls are serialized by the in-process backend, so their update functions are not retried with Firebase's captured contention counts. A synchronous re-entrant conflicting write does trigger a deterministic retry.
Oracle rtdb-modular-concurrent-transforms captures invocation counts [2, 3]; unit:modular/transaction-contention-cdd.test.ts pins the sandbox's [1, 1] ordinary-concurrency boundary and separately covers its deterministic re-entrant retry seam.
Production rejects an unindexed orderByValue() + limitToFirst(N) query; the sandbox does not enforce .indexOn and returns the N smallest primitive values
Oracle rtdb-modular-orderbyvalue-numeric captures production's Index not defined rejection; unit:modular/oracle-conformance-queries.test.ts pins that rejection beside the sandbox's successful [10,20,30] window. (Structured evidence: oracle-conformance-queries.test.ts)
**Divergence (pending fix):** clean goOffline, app deletion, playground pagehide, and best-effort MessagePort close drain queued operations, but unannounced total renderer/process loss is not guaranteed by the in-memory sandbox
Oracle rtdb-modular-ondisconnect-abrupt-exit proves Firebase executes an acknowledged registration after forced writer termination. The two-port worker integration exercises goOffline, served-app deletion, and non-persisted pagehide; browser MessagePort close delivery cannot prove total-process loss without durable host-owned leases. (Structured evidence: rtdb-integration.test.ts)
**Divergence:** DatabaseReference and Query equality includes database target, path, and canonical query parameters like Firebase, but toJSON() serializes the local sandbox://rtdb/... identity instead of Firebase's HTTPS database URL.
Oracle rtdb-modular-reference-shape-url captures conforming equality plus HTTPS JSON serialization; unit:modular/oracle-conformance-reference-writes.test.ts pins both the production HTTPS shape and the sandbox's sandbox:// boundary.
Production enforces .indexOn: '.value' for orderByValue() while the sandbox executes the value ordering without index enforcement
Oracle rtdb-modular-orderbyvalue-numeric rejects the unindexed production query; unit:modular/oracle-conformance-queries.test.ts pins the sandbox's successful ordered result instead of treating that contradiction as conformance.
**Divergence:** production interleaves concurrent increment writes from independent clients and preserves both deltas; the synchronous in-process backend applies each call before returning its promise, so a Promise.all call site is observably serialized even though its terminal value also reaches 5
Oracle rtdb-modular-concurrent-transforms uses two independent clients and captures terminal 5 after concurrent +2/+3 increments; unit:modular/transaction-contention-cdd.test.ts pins the sandbox's synchronous first-write visibility and matching terminal value without claiming that terminal equality proves contention semantics.
A cold production client can call the update fn with speculative null before the current seeded value; the always-warm sandbox calls it once with the current value
Oracle rtdb-modular-runtransaction-current-value-arg captures the cold two-call sequence and rtdb-modular-runtransaction-warm-client-speculation captures the matching warm one-call case; unit:modular/oracle-conformance-transactions.test.ts pins both sides. (Structured evidence: rtdb-modular-runtransaction-options-applylocally, rtdb-modular-runtransaction-success)
Concurrent contention — if another client writes between the read and write, the update fn is retried with the new current value (typically up to 25 retries by default)
Oracle rtdb-modular-concurrent-transforms captures callback counts [2, 3] across two clients; the synchronous in-process backend serializes the same ordinary calls with [1, 1], pinned by unit:modular/transaction-contention-cdd.test.ts.
goOffline(db) drains that client's onDisconnect queue once, but does not make the in-memory data plane unreachable; ordinary writes, listeners, and get() remain available
goOnline(db) reconnects the logical lifecycle without resurrecting drained operations; it is otherwise a no-op because the in-memory data plane never became unreachable
forceLongPolling() — accepted no-op: transport selection is not applicable to the in-process/worker sandbox (it never opens a real socket). Accepted so init code that calls it compiles + runs
forceWebSockets() — accepted no-op: transport selection is not applicable to the in-process/worker sandbox (see forceLongPolling)
enableLogging(logger?, persistent?) — accepted no-op: the sandbox has no modular-SDK-style logger to wire a level/sink into (it uses host-level console logging directly, matching pyric/firestore's setLogLevel). Accepted so init code that calls it compiles + runs
**Divergence:** refFromURL(db, url) matches Firebase's path parsing, query ignoring, FTP-to-database normalization, and fragment rejection, but the single-database sandbox does not validate the URL host/namespace against the handle; Firebase rejects a mismatched host while the sandbox uses its path.
Oracle rtdb-modular-reference-shape-url captures accepted FTP/query URLs and rejected fragments, malformed URLs, and host mismatches; unit:modular/oracle-conformance-reference-writes.test.ts pins the conforming validations plus both sides of the host divergence.