RFC: Live Query Framework Bindings Platform
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 266
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 55
Description
Live Query Framework Bindings Platform
Date: 2026-06-25
Summary
TanStack DB needs an official internal framework-binding platform, not more framework-by-framework ports of React hooks.
The current issue cluster spans React, Vue, Svelte, Solid, Angular, infinite queries, and SSR. The repeated root problem is that each adapter owns too much of the same semantic state machine: input normalization, collection ownership, sync activation, status/readiness/error tracking, result cardinality, initial snapshots, subscription timing, ordered materialization, pagination windows, SSR identity, and stale async protection.
This RFC proposes a full binding platform:
DbClient / scope / identity / hydration
│
▼
Internal LiveQueryObserver contract
│
├── stable snapshots
├── lifecycle + ownership
├── status/error/readiness
├── cardinality
├── non-reentrant subscriptions
└── stale async protection
│
▼
Optional shared controllers
├── layout revisions for ordered snapshots
└── infinite/windowed query controller
│
▼
Framework-native materializers
├── React
├── Vue
├── Svelte
├── Solid
└── Angular
│
▼
Shared adapter conformance suite
The key principle is:
Share DB/query semantics and lifecycle; keep renderer state management framework-native.
React should not be the reference implementation. Solid should not be the reference implementation. Vue should not be asked to match React or Solid. All official adapters should match the same internal observable contract while using their own native reactivity models.
getSnapshot() is the baseline semantic contract, not the whole materialization strategy. It gives every adapter a canonical, synchronous, revisioned view of a live query. It does not require Vue, Svelte, Solid, or Angular to become React-shaped useSyncExternalStore wrappers. Those adapters can still materialize the snapshot into native reactive objects, signals, stores, proxies, resources, or row-level subscriptions. Fine-grained materialization is a follow-up workstream, not a prerequisite for centralizing correctness.
Implementation progress
- Phase 1 — shared contract tests. #1636 added one conformance suite for React, Vue, Svelte, Solid, and Angular. It established the cross-adapter safety net and tracked known gaps explicitly.
- Phase 2 — shared adapter helpers. #1641 extracted the reactivity-free input, cardinality, and status helpers and adopted them across all five adapters.
- Phase 3 — shared observer and adapter migration. #1642 added
createLiveQueryObserverand moved all five adapters onto it while preserving each framework's native materialization model. - Phase 4 — ordered snapshot/layout contract. #1669 added transaction-scoped
layoutRevisionupdates for visible membership and ordering changes. It covers order-only moves, nested ordered includes, detached observers, window changes, and no-op transactions. The order-only-move conformance case now passes in all five adapters. - Phase 5 — shared infinite/window controller. #1675 added the shared controller and migrated React's infinite-query hook onto it.
- Phase 6 — SSR/client hydration.
- Phase 7 — docs and feature matrix.
Phases 1–5 are now merged into main. The conformance suite, shared helpers, observer, layout contract, and infinite/window controller form the base for the remaining adapter, hydration, and documentation work.
Context
This RFC comes from a cluster of issues and PRs that initially look framework-specific but point to one shared architectural problem: framework adapters are independently rebuilding the live-query lifecycle and result-materialization state machine. The same semantic gap then appears with different symptoms depending on the renderer.
In React, the failure mode is mostly around the external-store boundary. useLiveQuery can miss rows written between render and subscription, notify React before a component is committed, or trigger extra renders when a collection is already ready. Representative items include #439, #690, #1178, #1516, #1587, and #1594. These are not just timing bugs in one hook. They show that React currently observes a hook-local version counter and mutable collection state rather than one canonical live-query snapshot with well-defined subscription semantics.
In non-React adapters, the symptoms are different but the shape is familiar. Svelte has unsafe mutation and SSR-initialization problems when state is established through effect-time subscriptions (#985, #1196). Vue has adapter modernization work focused on shallow reactivity, lifecycle cleanup, disabled-query handling, and avoiding React-shaped patterns (#1302). Solid has proxy, keyed reconciliation, remount, and full-store-reset performance problems (#1598, #1608, #1609). Angular has seen cardinality drift where findOne() semantics are not consistently represented by the shared query result contract (#1272).
The infinite-query work shows the same duplication pressure. React's useLiveInfiniteQuery contains a substantial pagination/window state machine, and Vue/Svelte PRs are naturally tempted to port it (#1105, #1447, #1513). Bugs such as #1553 show that the active { offset, limit } window needs a single semantic owner; it should not be split between query-builder limits and hook-local setWindow() effects.
Core result publication also leaks into adapter behavior. #1601 shows that a live query can reorder without emitting a visible row-value change when the projected row is unchanged. Adapters cannot reliably render a change the core does not express. The required fix is a canonical observer snapshot whose layoutRevision, keys, and data change when the visible order changes, even if row values are unchanged.
SSR and identity are the other half of the same problem. #1564 moves toward DbClient, DbProvider, structured query IR identity, and row-based dehydration/hydration. That direction is compatible with this RFC, but the observer and adapter contracts may affect final SSR payload choices. Hydration cannot be solved independently in each framework adapter; it needs client-owned identity and snapshots that the adapters can consume consistently.
The current codebase already has separate framework adapters:
packages/react-db/src/useLiveQuery.tspackages/vue-db/src/useLiveQuery.tspackages/svelte-db/src/useLiveQuery.svelte.tspackages/solid-db/src/useLiveQuery.tspackages/angular-db
These adapters duplicate substantial behavior.
Examples observed in the current code:
- React
useLiveQuerydetects collections through method duck typing, starts sync during render, maintains a hook-local version counter, synthesizes a ready notification insubscribe, and currently callsuseSyncExternalStorewithout a server snapshot. - Vue and Svelte recreate similar input normalization, disabled-query behavior, eager sync startup, status tracking, and state/data materialization.
- Vue uses exception-driven disabled-query handling in one path.
- Svelte initializes data in
$effect, which cannot be the full SSR story and interacts poorly with unsafe mutation windows. - Solid uses
createStore/reconcileover full data snapshots, which is correct but can be expensive and has proxy/remount edge cases. - React infinite query owns a pagination state machine inside the hook, including page counts,
setWindow, stale promise protection, and serialized dependency comparison.
The conclusion is that the project needs a shared live-query protocol and conformance suite. Framework packages should not decide independently what counts as a disabled query, when a subscription may replay, whether findOne() returns one row or an array, how order-only moves are represented, or which lifecycle owns SSR identity.
PR #1564 is the current direction for DbClient, query identity, and SSR. This RFC builds on that direction but does not freeze every SSR detail. The observer and adapter contracts may influence the final SSR design, and Tanner/maintainers should be involved in that decision.
Goals
- Define an official internal live-query observer contract for first-party framework adapters.
- Move shared live-query semantics out of individual adapters.
- Preserve framework-native reactivity and scheduling.
- Make
DbClientthe owner of query identity, observer registry, scope, hydration, and shared lifecycle policy. - Normalize disabled queries, cardinality, status, errors, readiness, and cleanup across adapters.
- Support ordered result snapshots and layout revisions, including order-only moves.
- Move infinite/windowed query semantics into a shared internal controller.
- Make SSR/hydration client- and scope-first rather than React-hook-first.
- Establish a shared adapter conformance suite for React, Vue, Svelte, Solid, and Angular.
- Preserve existing public adapter APIs during migration where practical.
- Keep the door open for framework-native fine-grained materialization without making it part of the first milestone.
Non-goals
- Expose a public third-party adapter API.
- Make a universal renderer data structure that every framework wraps.
- Replace framework-native scheduling with a generic scheduler.
- Stabilize every SSR payload detail in this RFC.
- Implement all phases in one PR.
- Force every adapter to expose identical ergonomic APIs beyond shared semantics.
- Design the final patch/change-location API.
- Decide whether TanStack Store/State should become the shared materialization layer.
The observer contract is for official adapters. It can be exported where package boundaries require it, but it should be marked internal or unstable and not documented as an extension point for third-party adapters.
What this RFC does and does not do
This RFC centralizes the behavior that is easiest and most important to centralize now: identity, lifecycle, normalized inputs, disabled-query semantics, status/error/readiness, cardinality, synchronous snapshots, layout revisions, SSR snapshot hooks, stale generation protection, and cross-framework conformance tests.
It deliberately does not require a final fine-grained update model. Today Vue and Solid already use subscribeChanges directly to update framework-local state more finely than a whole React-style result object. That kind of fine-grained path should remain possible. The first milestone should not collapse every adapter into "replace the whole result object on every change" as a permanent design. It should establish the shared semantic clock and snapshot shape that any finer materializer can trust.
The follow-up work should be split out:
- Patch/change-location model: define how the observer reports inserts, deletes, row updates, order moves, and window membership changes when a framework can use that extra detail.
- Fine-grained materialization: decide how Vue/Svelte/Solid/Angular should materialize rows and properties into native reactive objects/signals/proxies.
- Shared state layer evaluation: test whether TanStack Store/State can be the adapter-facing materialization layer now that the Store/State architecture has changed. This should be validated against DB's distinctive update information rather than assumed.
Proposed architecture
1. DbClient and scope layer
DbClient should own:
- live-query identity
- observer registry
- request/client scope
- dehydration and hydration
- shared GC policy
- structured query IR identity where possible
- explicit user-provided identity for opaque functional queries
Framework adapters should not create, start, and dispose independent live-query collections directly during render or effect setup. They should resolve user input into a normalized spec, ask the client for an observer, and materialize that observer through native framework primitives.
2. Internal live-query observer
Introduce an internal observer contract along these lines:
interface InternalLiveQueryObserver<T, TKey> {
getSnapshot(): LiveQuerySnapshot<T, TKey>
getServerSnapshot(): LiveQuerySnapshot<T, TKey>
subscribe(listener: () => void): () => void
preload(): Promise<void>
update(spec: NormalizedLiveQuerySpec<T, TKey>): void
dispose(): void
}
The exact method list can evolve. The required behavior is the contract:
getSnapshot()is synchronous.- The same revision returns the same snapshot object.
- No observable change means no new revision.
- Every observable result, status, error, cardinality, readiness, or layout change publishes one atomic revision.
subscribe()does not synchronously replay initial state just because a collection is already ready.- Activation happens through retain/subscribe semantics, not render-time side effects.
- Disposal is idempotent.
- Disposed or stale generations cannot publish to newer observers.
- Errors are carried as
unknown, not reduced toisError: true. - Disabled queries are represented explicitly, not through thrown sentinel errors or framework-specific
nullconventions.
3. Snapshot model
The snapshot is the single source of truth for adapter-visible state.
Illustrative shape:
interface LiveQuerySnapshot<T, TKey> {
revision: number
layoutRevision: number
rowRevisions: ReadonlyMap<TKey, number>
status: 'disabled' | 'idle' | 'loading' | 'ready' | 'error' | 'cleaned-up'
isEnabled: boolean
isReady: boolean
error: unknown
cardinality: 'one' | 'many'
data: readonly T[]
keys: readonly TKey[]
collection: Collection<T, TKey, unknown> | undefined
}
The exact fields may change. The required concepts are:
- overall revision for any observable change
- layout revision for membership, order, or window changes
- row revisions for fine-grained row subscriptions
- cardinality so
findOne()is not reimplemented differently in every adapter - status and error in the snapshot
- ordered data and keys as the canonical result view
- stable snapshot identity for unchanged revisions
4. Normalized input contract
Adapters should not independently decide whether user input is a collection, config object, query callback, disabled query, or live-query collection.
The shared layer should normalize to a representation like:
type NormalizedLiveQuerySpec<T, TKey> =
| { enabled: false }
| {
enabled: true
collection: Collection<T, TKey, unknown>
ownership: 'external' | 'observer'
cardinality: 'one' | 'many'
}
Specific requirements:
- Provide one shared collection brand/type guard. Avoid
instanceof CollectionImpland per-adapter duck typing. - Call query factories in one well-defined place. Avoid probe-then-call-again behavior.
- Normalize legacy public overloads into one internal representation.
- Define disabled-query semantics once.
- Carry cardinality in the normalized spec/snapshot.
- Dependency tracking remains framework-native, but the resolved spec passed to the observer is consistent.
Renderer materialization
The shared layer should not expose a universal mutable Map plus mutable array. That would become a lowest-common-denominator renderer. It also should not imply that every framework adapter is a React-style whole-snapshot consumer forever.
Instead, the observer should expose stable snapshots as the required correctness contract. Adapters can render correctly from snapshots alone as long as every user-visible result change publishes a new revision and every membership, order, or window change publishes a new layoutRevision with updated keys and data.
Adapters may then choose a deeper native materialization strategy. Vue might materialize rows into shallow or deep reactive objects. Svelte might keep rune-safe state derived from the latest snapshot. Solid might reconcile keyed stores while preserving proxies for unaffected rows. Angular might expose signal-backed rows. The snapshot tells the adapter what is true; the adapter decides how to make that truth reactive.
Ordered result snapshots
Adapters need to know about the materialized result layout, not just row-value changes. A visible result can change because of:
- insert
- delete
- row update
- order-only move
- limit/window membership change
- reset after query/spec replacement
- status/error transition with unchanged rows
The required internal contract is snapshot-based:
interface LiveQuerySnapshot<T, TKey> {
revision: number
layoutRevision: number
data: readonly T[]
keys: readonly TKey[]
}
When order changes, layoutRevision, keys, and data change even if projected row values are unchanged. This is sufficient for correctness across all adapters.
An ordered patch stream may be added later as an internal optimization for large windows, row-level subscriptions, or renderer-specific performance work, but it is not required for the first binding-platform milestone.
React materializer
React should be a thin useSyncExternalStore adapter:
getSnapshot,subscribe, andgetServerSnapshotcome from the observer/client.- Returned result object is memoized by snapshot revision.
- Lazy
state/map view is allowed for compatibility. - Future fine-grained APIs should use selectors, key subscriptions, and row subscriptions rather than mutable lazy arrays.
- Render should be inert: no sync startup, listener attachment, GC scheduling, or ownership mutation during render.
Vue materializer
Vue should use Vue-native reactivity:
- shallow refs for the baseline ordered snapshot path
- effect-scope cleanup with
onScopeDispose - atomic application of observer snapshots
- shared normalization/branding instead of exception-based disabled-query handling or
instanceof CollectionImpl - optional later row/property-level materialization when Vue's deep reactivity is useful
The first Vue migration can be snapshot-based for correctness. A later Vue-specific pass should evaluate whether collection rows should be materialized into reactive row objects so Vue users can get fine-grained subscriptions on row properties without each component manually selecting from the whole result.
Svelte materializer
Svelte should be rune-safe and SSR-aware:
- initialize synchronously from
getSnapshot()before$effect - do not rely on
$effectfor SSR initial data - depend on non-reentrant
subscribe()behavior - apply updates outside unsafe mutation windows
- preserve Svelte 5 ergonomics while deriving semantics from the observer
Solid materializer
Solid should preserve fine-grained identity:
- keyed reconciliation with
$key - stable proxies for unaffected rows
- no full-store reset for one-row updates
- lazy keyed
statematerialization where compatibility requires it - layout/row revisions to choose between preserving state and resetting state
The first Solid migration can still consume snapshots. The important change is that the snapshot carries canonical revisions and layout revisions, so Solid's materializer can make better native choices without reimplementing live-query semantics. Later patch/change-location work can make Solid updates more surgical, but Solid correctness should not wait for that.
Angular materializer
Angular should be first-class:
- signal-backed result state
- injector-aware lifecycle and cleanup
- shared cardinality so
findOne()returns the correct runtime shape - participation in the same conformance matrix as React, Vue, Svelte, and Solid
Infinite and windowed queries
useLiveInfiniteQuery should not be a React hook state machine copied into Vue and Svelte. Pagination/window semantics should live in a shared internal controller layered on top of the observer/client.
Illustrative contract:
interface InternalLiveInfiniteQueryObserver<T, TKey> {
getSnapshot(): LiveInfiniteQuerySnapshot<T, TKey>
subscribe(listener: () => void): () => void
fetchNextPage(): Promise<void>
reset(): void
dispose(): void
}
The controller should own:
- loaded page count
- pages
- page params
hasNextPageisFetchingNextPage- pagination errors
- peek-ahead row
- active
{ offset, limit }window - stale
setWindow()promise protection - reset behavior when the underlying query generation changes
- validation that the query is ordered/window-capable
- serializable pagination state for hydration, if supported
Required API decisions:
fetchNextPage()returnsPromise<void>.- Pagination errors are exposed in the snapshot, not only logged.
getNextPageParamis either implemented or deprecated/removed before the API stabilizes.- The pagination controller does not use
JSON.stringify(deps). - For infinite queries, the controller is the single authority for
{ offset, limit }; builder.limit().offset()must not compete with latersetWindow()calls.
Migration stance:
- Narrow correctness fixes can land when needed.
- Long-term Vue/Svelte infinite query support should not copy React's hook-local state machine.
- Existing React implementation and historical fixes should become requirements/tests, not the canonical implementation.
SSR, identity, and hydration
The RFC assumes DbClient is the current direction for identity and SSR, but the final API should be decided with Tanner and maintainers after reviewing how the observer contract affects hydration.
Target model
SSR should be scope/client-first, not React-hook-first.
Server:
const client = createDbClient()
const todos = client.getCollection(todoDefinition, params)
const openTodos = client.getLiveQuery(querySpec)
await openTodos.preload()
const payload = client.dehydrate()
Client:
const client = createDbClient()
client.hydrate(payload)
Framework adapters then read first snapshots from hydrated observers.
Requirements
getServerSnapshot()exists for adapters that need it.- Server snapshot and first client snapshot agree when hydration data is present.
- Client-only mode has a stable server/first-client empty or loading snapshot.
- Hydration payloads are versioned.
- Request-scoped clients are isolated by default.
- Process-wide reuse is explicit.
- Hydration carries enough revision/layout identity to avoid false immediate rerenders.
- Derived live-query rows may be omitted when source collection snapshots are included and the client can rebuild them deterministically.
- Sync resume metadata can be included where collection implementations support it.
- RSC/server components use one-shot scoped evaluation or prefetch; they do not instantiate enduring client-style subscriptions.
Maintainer decision: payload policy
The RFC should not force one answer before maintainer review. The key decision is whether DbClient dehydrates:
- live-query result snapshots,
- source collection rows,
- or both with pruning.
Trade-offs:
- Result snapshots are smaller and match hook hydration directly.
- Source rows are more canonical and let the client rebuild derived query/dataflow state.
- A hybrid payload can prune derived rows when source rows are already present.
The recommended direction is source-first or hybrid, but the final policy should be decided with Tanner and maintainers.
Framework provider surfaces
Each adapter should expose a framework-native way to provide the client:
- React:
DbProvider - Vue: provide/inject
- Svelte: context
- Solid: context
- Angular: DI provider
The SSR data model belongs in core/client. Framework packages should not invent independent hydration formats.
Conformance suite
The conformance suite is part of the architecture, not a follow-up nicety. The project needs a shared behavioral contract, not a reference framework.
Each official adapter should provide a harness with operations such as:
- mount/use a live query
- read current result
- update source collection
- flush framework work
- render server-side, where applicable
- hydrate, where applicable
- unmount/dispose
The same scenarios should run against React, Vue, Svelte, Solid, and Angular.
Required coverage:
- query callback, config object, direct collection, and nullable/disabled query
- disabled → enabled → disabled transitions
- collection replacement and stale callbacks
- idle/loading/ready/error/cleaned-up statuses
- already-ready collection at mount
- no synchronous replay during subscribe
- no missed render-to-subscribe updates
- no updates after unmount/dispose
- StrictMode or equivalent lifecycle stress where applicable
findOne()runtime shape and type inference- insert, update, delete, order-only move, and window membership changes
- SSR server snapshot and hydration equality
- row identity preservation for unaffected rows
- infinite query window expansion and stale
setWindow()promises - error propagation and suspense/resource behavior where applicable
Executable invariants should include:
same revision => Object.is(previousSnapshot, nextSnapshot)
observable change => exactly one later revision
no observable change => no revision
unsubscribe => no future callback
render => no sync or ownership side effects
retain/release => balanced after every mount sequence
SSR snapshot => equals first hydration snapshot when hydrated
Migration plan
Phase 1: write contract tests
Encode desired behavior before moving adapter code.
Include known regressions from the issue cluster:
- React render-to-subscribe gap
- React pre-mount eager notification
- React extra ready rerender
- React StrictMode extra subscriber
- missing server snapshot behavior
- Svelte unsafe mutation / synchronous initialization
- Vue disabled-query and lifecycle behavior
- Solid row identity/proxy/remount behavior
- Angular
findOne()cardinality - order-only move publication
- stale
setWindow()promises
Phase 2: introduce internal observer and normalized input contract
Add:
- shared collection branding/type guard
- normalized live query spec
- disabled query representation
- cardinality in snapshot
- status/error/readiness in snapshot
- non-reentrant subscription behavior
- retain/release lifecycle
- stale generation protection
Phase 3: migrate all five adapters
Migrate React, Vue, Svelte, Solid, and Angular to consume the observer while preserving public APIs where practical.
Remove duplicated lifecycle/state-machine code from adapters. Keep renderer-native materializers.
Phase 4: add ordered snapshot/layout contract
Support order-only moves and layout revisions directly. Ensure revision, layoutRevision, keys, and data update for all visible membership, ordering, and window changes. This is a correctness milestone, not a patch-stream optimization milestone.
Use this to improve:
- React external-store consistency
- Vue shallow/atomic snapshot updates
- Svelte safe snapshot updates
- Solid keyed reconciliation correctness
- Angular signal updates
Phase 5: extract infinite/window controller
Move pagination/window ownership out of React and into a shared internal controller.
Finish Vue and Svelte infinite-query support on top of shared semantics.
Phase 6: finalize SSR/client hydration
Integrate with the #1564 direction or with a revised maintainer decision.
Add provider/context/DI wrappers across adapters and SSR e2e coverage.
Phase 7: docs and feature matrix
Document shared semantics once. Framework docs should focus on native usage examples.
Add a feature matrix covering:
useLiveQuery- infinite/windowed queries
- SSR/hydration
- disabled queries
- suspense/resource behavior
- single-result behavior
- fine-grained subscriptions/selectors
Follow-up RFCs: fine-grained updates and shared materialization
After the observer/conformance baseline lands, split the fine-grained work into separate RFCs rather than bundling it into this one:
- A patch/change-location RFC for ordered result changes, row changes, and window membership changes.
- A framework materialization RFC for Vue/Svelte/Solid/Angular deep reactivity, row objects, property subscriptions, selectors, and range/key subscriptions.
- A TanStack Store/State evaluation RFC or spike to decide whether Store/State can be used as the adapter-facing materialized collection state.
Treatment of current open work
- Vue modernization work should inform the Vue materializer, especially shallow reactivity, effect-scope cleanup, stale collection guards, and removal of deep proxying.
- Solid renderer rework should inform the snapshot/layout contract and Solid materializer, especially
$key, proxy preservation, lazystate, and avoiding unnecessary full resets. - React eager-notification and render-to-subscribe fixes should become conformance tests. Microtask deferral can be a tactical fix but should not be the final semantic model.
- Order-only move fixes should become an explicit layout-revision requirement, not a hidden forced-update path.
- Vue/Svelte infinite query PRs should be rebased onto the shared infinite/window controller rather than copying React's state machine.
- DbClient/SSR work in #1564 is the current foundation, but final hydration payload policy remains a maintainer decision.
Patch and fine-grained materialization deferral
A patch stream is not required for correctness in the first milestone. UI-facing live queries usually have small active windows, and adapters already render from snapshots or snapshot-like materializations today. A canonical snapshot with revision, layoutRevision, keys, and data is enough to fix the semantic bugs, including order-only moves, as long as every visible layout change publishes a new snapshot.
The remaining requirement is that core detects visible layout changes. For example, when an orderBy field changes but the projected row value is unchanged, the observer must still publish a new snapshot with an incremented layoutRevision and updated order. It does not need to expose a { type: "move" } patch initially.
Fine-grained update location is still important; it is just a separate layer. Vue and Solid already use subscribeChanges directly today for more targeted updates, and future adapters should be able to keep or improve that path. A later patch/change-location design can describe inserts, deletes, updates, moves, range/window membership changes, and row/key subscriptions. That later design can build on the same revision and layout-revision contract without blocking the observer, adapter migration, infinite-query controller, or SSR work.
A separate materialization investigation should also test whether TanStack Store/State can be used for adapter-facing collection materialization. The old blocker was immutable store replacement; if the newer Store/State work supports mutable signal-style updates, it may provide shared framework reactivity for free. That should be verified with TanStack DB's ordering/window/cardinality update model before becoming part of the platform.
Risks and mitigations
Risk: the RFC is too broad
Mitigation: treat this as one platform RFC with staged implementation. The first implementation milestone is the observer contract plus conformance tests.
Risk: internal observer leaks as public API
Mitigation: mark it internal/unstable and document that it is for official adapters only. Avoid public adapter-author documentation.
Consideration: centralized snapshots should not preserve today's avoidable costs
Adapters already generate snapshots or snapshot-like materializations today. React memoizes a hook-local external-store snapshot and captures Array.from(collection.entries()) for each returned result revision. Vue, Svelte, and Solid also maintain framework-local ordered data and keyed state derived from collection contents. Query snapshot cost is not expected to be a primary risk because UI-facing live queries usually have small active windows.
The design should still avoid carrying forward unnecessary duplication. The central observer snapshot should be cheaper and more canonical than today's per-adapter snapshots by using stable identity for unchanged revisions, structural sharing for data and keys, layout revisions, row revisions, and lazy state/map compatibility views.
Risk: SSR decisions block adapter fixes
Mitigation: separate the observer/conformance milestone from final SSR payload policy. Keep DbClient as the assumed owner while leaving hydration details open for maintainer review.
Risk: adapters lose framework idioms
Mitigation: share semantics, not renderer state. Each adapter keeps native scheduling/materialization.
Risk: migration breaks public APIs
Mitigation: preserve public overloads initially and normalize internally. Deprecations can come later after conformance is in place.
Alternatives considered
Observer contract only
A narrower RFC could define only InternalLiveQueryObserver and migrate adapters to it.
This is the best first implementation milestone, but too narrow as an RFC because observer decisions affect ordered changes, infinite queries, SSR snapshots, and conformance.
DbClient/SSR first
A narrower RFC could center on DbClient, identity, and hydration.
This aligns with current momentum but does not fix the duplicated adapter state machines or cross-framework behavior drift.
React as reference implementation
React could continue as the de facto source of truth, with other adapters porting its logic.
This has already produced drift and framework-shaped bugs. React's external-store model is not the right model for every renderer.
Universal renderer state
Core could expose one mutable Map and one ordered array for all adapters.
This would simplify code superficially but would fight framework-native reactivity and likely reproduce Solid/Vue/Svelte performance and lifecycle issues in a different form.
Recommendation
Adopt the full binding platform design as the architectural direction.
Implement it in phases:
- contract tests,
- internal observer and input normalization,
- adapter migration,
- ordered snapshots and layout revisions,
- infinite/window controller,
- SSR/hydration finalization,
- docs and feature matrix.
Then follow with separate RFCs for patch/change-location APIs, framework-native fine-grained materialization, and possible TanStack Store/State integration.
The decisive shift is from “port hooks from React” to “implement one internal live-query protocol and several native renderers.” Pagination, SSR, query cardinality, disabled semantics, stale async behavior, and lifecycle correctness can then be fixed once while keeping each framework adapter idiomatic.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing adapter entry points in packages/react-db/src/useLiveQuery.ts, packages/vue-db/src/useLiveQuery.ts, packages/svelte-db/src/useLiveQuery.svelte.ts, packages/solid-db/src/useLiveQuery.ts, and packages/angular-db, together with the completed phases in this RFC. Done requires an agreed scope for SSR/client hydration and documentation or feature-matrix work, plus maintainer decisions on the remaining observer and adapter contracts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, react, typescript
- Domain
- developer-experience, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100