Skip to content

draft designs for SSR#1256

Open
samwillis wants to merge 2 commits intomainfrom
ssr2
Open

draft designs for SSR#1256
samwillis wants to merge 2 commits intomainfrom
ssr2

Conversation

@samwillis
Copy link
Collaborator

@samwillis samwillis commented Feb 17, 2026

This PR contains two draft designs for SSR, along with examples of how they work.

SSR_DESIGN.md was the first based on the direction from the original issue #545 and draft PR #709.

SSR_ALT_DESIGN.md is taking a step back, noting that live queries are collections, and we need serialisation and scoping for both. It unifies the system.


SSR_DESIGN.md -- The Extension Approach

Origin: Extends the direction proposed in issue #545 and draft PR #709, which introduced prefetchLiveQuery, createServerContext, dehydrate, and HydrationBoundary. The original issue focused on hydrating useLiveQuery results by query id, similar to how TanStack Query handles SSR. This design adds request-scoped collections on top of that foundation to solve the data-leakage problem.

Core idea: Every server collection declares its scope as request (default, isolated per request) or process (shared across requests for cacheable data). A DbSharedEnvironment manages process-scoped singletons via getOrCreate. A DbRequestScope holds named collections with declared scopes and tracks prefetched queries. Dehydration is a separate step (dehydrateDbScope) with fine-grained options for which collections and queries to include.

API surface: ~12+ primitives -- createDbSharedEnvironment, createDbRequestScope, DbSharedEnvironment, DbRequestScope, prefetchDbQuery, dehydrateDbScope, CollectionSelector, DehydrateDbScopeOptions, CollectionSnapshot, exportSnapshot, importSnapshot, SyncConfig, HydrationBoundary, useHydratedQuery, useHydrateCollections, plus the collection factory pattern (createServerCollections).

Serialization model: Two separate systems -- query result dehydration (by query id) and collection snapshot dehydration (opt-in allowlist). ssr.explicitlySerialized on a query skips auto-marking its source collections as "used," avoiding duplicated payloads. Collection snapshots have their own exportSnapshot/importSnapshot API with sync resume policies (resume-if-possible, truncate, throw, ignore).

Strengths: Comprehensive sync resume semantics, detailed security/payload controls, explicit request/process scope declarations, phased rollout plan with backwards compatibility.

Weaknesses: High ceremony -- the collection factory pattern, shared environment setup, scope declaration map, and separate dehydration options add significant boilerplate. Two distinct serialization paths (queries vs collections) with different APIs and different opt-in mechanisms. The developer must understand which path applies when.


SSR_ALT_DESIGN.md -- The Redesign

Origin: A step back from the extension approach, rethinking the problem from first principles. Aims for a smaller, more consistent API where collections and live queries are treated uniformly through the same getter/scope pattern.

Core idea: Two new definition helpers (defineCollection, defineLiveQuery) produce getter functions that accept an optional scope argument. Passing scope binds the instance to a request lifecycle; omitting it falls back to global/process memoization. Serialization intent is expressed through scope.include(collection) for collection snapshots and ssr: { serializes: true } for live query results. A single scope.serialize() call produces the complete dehydrated payload, with an automatic pruning step that skips live query payloads when their source collections are already included.

API surface: 5 core primitives -- createDbScope, ProvideDbScope, useDbScope, defineCollection, defineLiveQuery. The scope object has 3 methods: include, serialize, cleanup.

Serialization model: One unified system. Both collection snapshots and live query results go into a single DehydratedDbStateV1 payload. The pruning logic at serialize() time automatically avoids duplicate data: if a live query's source collections are all included as snapshots, the live query result is omitted (the client re-derives it from the hydrated collections).

Scope placement: Two strategies depending on framework. Single root scope (preferred, used in TanStack Start where createRouter() runs per request) or per-loader scope with nested ProvideDbScope merge (used in React Router/Remix and Next.js where framework constraints prevent a shared scope). Merge uses timestamp-based freshness to resolve conflicts.

Sync resume: Optional exportSyncMeta/importSyncMeta hooks on SyncConfig allow sync layers to export and restore resume metadata via the meta field in DehydratedDbStateV1. Query collections populate the QueryClient cache on import so staleTime prevents immediate refetch (always resumable). Electric collections export seenTxids/seenSnapshots and a shape offset when available; full resume is blocked in v1 because ShapeStream does not expose its running offset, but adding a getCurrentOffset() accessor upstream would unblock it with no design change needed here. Live queries always restart from scratch -- the D2 graph has internal state (multiplicity tracking, join buffers) that cannot be serialized, so it must be rebuilt from current source rows. Source resumability affects transition quality: if all sources resume, the D2 first run matches hydrated data seamlessly; if any source restarts, the imported live query result provides stable UI while that source re-syncs. Fallback is always safe: hydrated rows remain as placeholders until fresh sync data overwrites them.

Strengths: Small API surface, consistent treatment of collections and live queries, the getter pattern preserves existing global-import usage for non-SSR code, scope is optional so adoption is incremental, automatic payload pruning reduces developer decisions about what to serialize, sync resume is opt-in per sync layer with a safe fallback.

Weaknesses: Scope threading is manual in client components (useDbScope() + pass to every getter). Per-loader scope strategy means duplicate server-side fetches when multiple loaders use the same collection. Full Electric resume depends on an upstream @electric-sql/client API addition.


Key Differences at a Glance

SSR_DESIGN (extension) SSR_ALT_DESIGN (redesign)
Primitives ~12+ 5
Scope declaration Explicit request/process map per collection Implicit: pass scope = request, omit = global
Collection definition Factory function (createServerCollections) defineCollection / defineLiveQuery getters
Serialization Two paths: query dehydration + collection snapshot dehydration One unified scope.serialize() with automatic pruning
Process-scoped data DbSharedEnvironment + getOrCreate Omit scope from getter call
Transfer opt-in dehydrateDbScope options + ssr.explicitlySerialized scope.include(collection) + ssr: { serializes: true }
Sync resume Full policy matrix (resume-if-possible, truncate, throw, ignore) exportSyncMeta/importSyncMeta hooks; Query always resumes, Electric best-effort (upstream blocker), live queries always restart D2 from scratch
Nested routes Not addressed ProvideDbScope merge with timestamp-based freshness
Framework strategies Same pattern everywhere (factory + scope map) Framework-specific: single root scope vs per-loader merge

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

⚠️ No Changeset found

Latest commit: eaff541

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 17, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1256

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1256

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1256

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1256

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1256

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1256

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1256

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1256

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1256

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1256

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1256

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1256

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1256

commit: eaff541

@github-actions
Copy link
Contributor

Size Change: 0 B

Total Size: 92.1 kB

ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.22 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.75 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/subscription.js 3.71 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.7 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.69 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 B
./packages/db/dist/esm/local-only.js 808 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/proxy.js 3.75 kB
./packages/db/dist/esm/query/builder/functions.js 733 B
./packages/db/dist/esm/query/builder/index.js 4.09 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.43 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.81 kB
./packages/db/dist/esm/query/compiler/index.js 2.02 kB
./packages/db/dist/esm/query/compiler/joins.js 2.11 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/compiler/select.js 1.06 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.44 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 2.42 kB
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/optimizer.js 2.62 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.3 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils.js 924 B
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/comparison.js 952 B
./packages/db/dist/esm/utils/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

Size Change: 0 B

Total Size: 3.7 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant