Sync & Realtime
Typed TanStack DB collections over the oRPC Publisher transport
bunderstack/sync layers TanStack DB collections over the same generated oRPC
procedures. It provides local live queries, optimistic mutations, scoped
windows, and reliable realtime updates inferred from your server app.
Create a sync client
bun add bunderstack @tanstack/react-query @tanstack/db @tanstack/query-db-collectionimport { createSyncClient } from 'bunderstack/sync'
import type { App } from './bunderstack'
const api = createSyncClient<App>({ queryClient })Each exposed table receives:
api.posts.collection
api.posts.table
api.posts.scopedCollection(options)
api.posts.collectionByIds(ids)Collections are stable by configuration. Calling scopedCollection() or
collectionByIds() with equivalent options returns the existing collection;
do not cast a plain array into a collection and do not rebuild collections on
every render.
Optimistic mutations and reconciliation
api.posts.collection.insert({ id, title, userId })
api.posts.collection.update(id, (draft) => {
draft.title = 'renamed'
})
api.posts.collection.delete(id)The server mutation returns the canonical row after defaults, ownership, and database transforms are applied. The sync layer reconciles that row into every materialized view without a follow-up list request. Several overlapping local edits to the same row are coalesced so a stale completion cannot overwrite a newer optimistic state.
Scoped collections
Use a growing window for feeds and other ordered datasets:
const feed = api.posts.scopedCollection({
filters: { replyToId: null },
sort: 'createdAt',
order: 'desc',
initialCount: 20,
})
const result = useLiveQuery((q) =>
q
.from({ post: feed.collection })
.orderBy(({ post }) => post.createdAt, 'desc'),
)
await feed.loadMore()
feed.hasMore()Use collectionByIds(ids) for exact relation lookups rather than searching a
capped base collection:
const authorIds = [...new Set(posts.map((post) => post.userId))].sort()
const authors = api.user.collectionByIds(authorIds)One realtime path
On the server, generated writes and context.realtime.publish() emit typed
changes through oRPC Publisher. The built-in realtime.changes procedure
returns an event iterator. On the client, Bunderstack applies each change to
query caches and all materialized collection views.
database write
→ oRPC Publisher
→ realtime.changes iterator
→ query cache + TanStack DB collectionsRealtime starts automatically in browser sync clients and stays disabled during SSR. Applications do not poll a list endpoint and do not create a second event client.
Reliability behavior
The library owns the transport lifecycle:
- an internal heartbeat keeps quiet streams observable without producing application events or cache work;
- a stream that stops delivering is detected and replaced. The server sends a heartbeat on a fixed interval and advertises that interval on the event itself. The client tears the connection down after 2.5 intervals of silence and reconnects. Without this a connection killed by a proxy idle-timeout, a sleeping laptop, or a network change would hang open and deliver nothing;
- failed connections retry with exponential backoff and jitter rather than a fixed request loop;
- Publisher event IDs allow resume while retained events are available;
- after reconnect, subscribed tables are refetched once so an expired replay window cannot leave the cache stale;
- changes are applied in one batch per animation frame, so a burst of writes costs one cache write and one invalidation per query key rather than one of each per event;
- a heartbeat is filtered inside the transport and never appears in a live query.
Publisher replay is an optimization. The database and the reconnect refetch remain the source of cache correctness.
Custom publications
Publish a canonical row from a procedure or job when a write happens outside generated CRUD:
await context.realtime.publish(schema.posts, 'update', updatedPost)Use Redis realtime configuration when web and worker processes are separate; the memory publisher is process-local.
For TanStack Query without collections, use syncRealtime from
bunderstack/query. Framework adapters are covered in
Framework Portability.