$39
Bootstraps a complete local-first data layer: server publishes changes via SSE, client stores data in IndexedDB via RxDB, TanStack DB provides reactive queries in React.
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (Web) │
├─────────────────────────────────────────────────────────────────┤
│ React Components │
│ │ │
│ ▼ │
│ useLiveQuery() ◄──── @tanstack/react-db │
│ │ │
│ ▼ │
│ TanStack DB Collections ◄──── @tanstack/rxdb-db-collection │
│ │ │
│ ▼ │
│ RxDB (IndexedDB via Dexie) ◄──── Replication Plugin │
│ │ │ │ │
│ │ (pull handler) │ (SSE stream$) │ (push) │
└───────┼──────────────────────────┼────────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ SERVER (Hono) │
├─────────────────────────────────────────────────────────────────┤
│ oRPC Router │
│ │ │
│ ├── sync.pull (checkpoint-based pagination) │
│ ├── sync.push (conflict-aware writes) │
│ └── sync.stream (multiplexed SSE via EventPublisher) │
│ │ │
│ ▼ │
│ EventPublishers ◄──── Mutations publish events │
│ │ │
│ ▼ │
│ mergeAsyncIterables ──── Single SSE connection │
│ │ │
│ ▼ │
│ Drizzle ORM ◄──── SQLite / D1 / Postgres │
└─────────────────────────────────────────────────────────────────┘
# Server
bun add @orpc/server drizzle-orm zod
# Client
bun add rxdb rxjs @tanstack/react-db @tanstack/rxdb-db-collection
For each syncable entity, create:
Publisher (packages/api/src/publishers/[entity]-publisher.ts)
EntityReplicationDoc interface with _deleted flagEventPublisher keyed by userIdtoEntityReplicationDoc() helper (Date -> timestamp ms)Sync endpoints in router (packages/api/src/routers/[entity].ts)
sync.pull: Checkpoint-based pagination with updatedAt + id compound cursorsync.stream: SSE endpoint using eventIterator() + publisher subscriptionPublish events in mutation handlers (create/update/delete)
See references/server-publishers-and-endpoints.md for complete patterns.
Combine all entity SSE streams into a single connection to stay within browser limits (~6 per origin).
mergeAsyncIterables() helper + syncRouter.stream endpointcreateDemultiplexedStreams() — single SSE -> per-entity RxJS SubjectsSee references/multiplexed-streaming.md for complete patterns.
_deleted: boolean, timestamps as numbercreateRxDatabase with getRxStorageDexie()getOrCreateDb()See references/client-rxdb-setup.md for complete patterns.
createPullStream() or createPullStreamWithResync()replicateRxCollection() per entity with pull handler + stream$pull.stream$received$.subscribe() for dependent entitiesSee references/client-replication.md for complete patterns.
For entities where the client creates/modifies data locally:
sync.push with conflict detection via updatedAt comparisonpush.handler(changeRows) in replication configSee references/push-replication.md for complete patterns.
createCollection(rxdbCollectionOptions({ rxCollection }))DbProvider + useDbCollections() hookbeforeLoad, wrap layout with DbProvideruseLiveQuery() — SQL-like API: .from(), .where(), .innerJoin(), .select()See references/react-db-integration.md for complete patterns.
const checkpointSchema = z.object({ id: z.string(), updatedAt: z.number() }).nullable();
async handler(checkpointOrNull, batchSize) {
const checkpoint = checkpointOrNull ?? null; // RxDB undefined -> oRPC null
const response = await client.entity.sync.pull({ checkpoint, batchSize });
return { documents: response.documents, checkpoint: response.checkpoint ?? undefined };
}
async handler(changeRows) {
const response = await client.entity.sync.push({
rows: changeRows.map((row) => ({
newDocumentState: row.newDocumentState,
assumedMasterState: row.assumedMasterState ?? null,
})),
});
return response.conflicts;
}
const { data } = useLiveQuery(
(q) => q.from({ entity: entityCollection }).where(({ entity }) => eq(entity.status, "active")),
[/* deps */],
);
packages/api/src/
├── publishers/
│ ├── [entity]-publisher.ts # EventPublisher + types + toReplicationDoc
│ └── multiplexed-publisher.ts # mergeAsyncIterables + MultiplexedStreamEvent
├── routers/
│ ├── [entity].ts # sync.pull + sync.push + sync.stream
│ └── sync.ts # Multiplexed SSE endpoint
└── index.ts # protectedProcedure export
apps/web/src/lib/db/
├── db.ts # RxDB schemas + react-db collections + singleton
├── db-context.tsx # DbProvider + useDbCollections
├── replication.ts # Pull streams + replication setup
└── multiplexed-replication.ts # Demultiplexer (single SSE -> per-entity Subjects)
apps/web/src/hooks/
└── use-[entity].ts # Custom useLiveQuery hooks