.claude/skills/local-first-replication/SKILL.md
Bootstraps local-first replication architecture using RxDB + oRPC + TanStack DB in TanStack Router applications. Covers server-side EventPublisher pattern, checkpoint-based pull endpoints, push replication with conflict resolution, multiplexed SSE streaming (single connection for all entities), client-side RxDB setup with IndexedDB, replication configuration, and React integration with TanStack DB collections and useLiveQuery. Use when: (1) Setting up offline-first architecture from scratch, (2) Adding real-time replication to a TanStack Router + oRPC app, (3) Bootstrapping a local-first data layer, (4) Implementing multiplexed sync to avoid browser connection limits, (5) Adding push replication with conflict resolution, (6) Creating reactive queries with RxDB + TanStack DB. Triggers: "set up local-first", "bootstrap offline-first", "add replication architecture", "set up RxDB with oRPC", "create local-first architecture", "implement pull replication", "implement push replication", "add multiplexed sync", "set up tanstack db with rxdb", "add real-time replication", "offline-first setup".
npx skillsauth add WonderPanda/mana-vault local-first-replicationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
development
Set up worktrunk (git worktree manager) with dynamic port allocation for Better-T-Stack monorepos using Doppler, Alchemy, and Vite. Use when bootstrapping a new project's worktree workflow, or when asked to "set up worktrunk", "configure worktrees", "add worktree support", or "set up dynamic ports for worktrees". Covers: post-create hooks (Doppler setup, dependency install, .env.worktree generation), env-based port overrides in Vite and Alchemy configs, and dev script modification to source worktree-specific env vars after Doppler injection.
tools
TanStack DB client-side reactive data patterns for Mana Vault. Use when working with useLiveQuery hooks, client-side collections, RxDB/Dexie sync, or any reactive data queries in the web app. Covers hook locations, DB setup, and query builder best practices.
development
React Native and Expo patterns for Mana Vault mobile app. Use when working on the native app in apps/native/, modifying Expo Router routes, HeroUI Native components, or Uniwind styling. Covers project structure and key conventions.
development
React component patterns, styling conventions, and UI guidelines for Mana Vault web app. Use when creating or modifying React components, working with TailwindCSS, shadcn/ui, class-variance-authority, or route components in TanStack Router.