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().
unit:modular/sandbox-target.test.ts (snapshot shape tests)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.jsonremove(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.jsonupdate(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 contractOverlapping 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.jsonpush(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 contractserverTimestamp() 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.jsonRules-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.jsonA 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 claimCommitted 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.jsonorderByKey() 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-276Production 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.jsononValue(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-230Read-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-230null 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-126Write 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-199Conflicting 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-2206push(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-630DataSnapshot 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-400A 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-980Child-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.
oracle:
packages/conformance/observations/rtdb-modular/rtdb-modular-get-snapshot-shape.json (Structured evidence: snapshot-shape.test.ts)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
oracle:
packages/conformance/observations/rtdb/rtdb-rules-denied-error-code.json (Structured evidence: lifecycle-and-identity.test.ts)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)
oracle:
packages/conformance/observations/rtdb/rtdb-rules-denied-error-code.json (Structured evidence: public-operations.test.ts)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
oracle:
packages/conformance/observations/rtdb/rtdb-remove-vs-set-null.json (Structured evidence: public-operations.test.ts)Idempotent — remove on a path that's already absent resolves successfully (no-throw)
oracle:
packages/conformance/observations/rtdb-modular/rtdb-modular-remove-idempotent.json — remove 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
oracle:
packages/conformance/observations/rtdb/rtdb-remove-vs-set-null.json (Structured evidence: public-operations.test.ts)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)
oracle:
packages/conformance/observations/rtdb/rtdb-push-autoid-format.json (Structured evidence: public-operations.test.ts)Sequential push calls produce monotonically-sortable keys (timestamp-prefixed for chronological ordering via orderByKey)
oracle:
packages/conformance/observations/rtdb/rtdb-push-autoid-format.json (Structured evidence: public-operations.test.ts)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.json — await 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.
Oracle observations
rtdb-modular-onchildmoved-with-orderby, rtdb-modular-onchildmoved-previouschildname-sequencing, and rtdb-modular-childchanged-cofire-with-childmoved are replayed by the modular oracle suite. (Structured evidence: oracle-conformance-listeners.test.ts)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.json — unsubReturnType: '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.json — startAt(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.json — startAfter(2) + endBefore(5) matched positions [3,4] (cursor 2 dropped). (Structured evidence: public-operations.test.ts)endBefore(v) is exclusive
oracle:
packages/conformance/observations/rtdb-modular/rtdb-modular-startafter-endbefore-exclusive.json — same probe; cursor 5 dropped. (Structured evidence: public-operations.test.ts)limitToFirst(N) caps the result count from the start of the ordered range
oracle:
packages/conformance/observations/rtdb-modular/rtdb-modular-query-orderbychild-limit.json plus packages/conformance/observations/rtdb-modular/rtdb-modular-limittofirst-vs-limittolast.json (firstPositions [1,2]). (Structured evidence: rtdb-modular-orderbyvalue-numeric, public-operations.test.ts)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)
oracle:
packages/conformance/observations/rtdb-modular/rtdb-modular-runtransaction-success.json — observed snapVal: 1 matching the committed value. (Structured evidence: rtdb-modular-runtransaction-returns-committed-snapshot, public-operations.test.ts)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.