{"id":51341913,"url":"https://github.com/doeixd/tanstackstart-db","last_synced_at":"2026-07-02T08:33:33.023Z","repository":{"id":362310948,"uuid":"1257546906","full_name":"doeixd/tanstackstart-db","owner":"doeixd","description":"Typed schema, view, query, action, route, and hydration helpers for TanStack DB","archived":false,"fork":false,"pushed_at":"2026-06-03T15:24:31.000Z","size":222,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-03T16:06:40.983Z","etag":null,"topics":["crud","hydration","optimistic-updates","react","schema","ssr","tanstack","tanstack-db","tanstack-start","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doeixd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-02T19:31:31.000Z","updated_at":"2026-06-03T15:32:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doeixd/tanstackstart-db","commit_stats":null,"previous_names":["doeixd/tanstackstart-db"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/doeixd/tanstackstart-db","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Ftanstackstart-db","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Ftanstackstart-db/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Ftanstackstart-db/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Ftanstackstart-db/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doeixd","download_url":"https://codeload.github.com/doeixd/tanstackstart-db/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Ftanstackstart-db/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35040021,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crud","hydration","optimistic-updates","react","schema","ssr","tanstack","tanstack-db","tanstack-start","typescript"],"created_at":"2026-07-02T08:33:32.105Z","updated_at":"2026-07-02T08:33:33.006Z","avatar_url":"https://github.com/doeixd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tanstackstart-db\n\nSchema-first application data helpers for [TanStack DB](https://tanstack.com/db).\n\n`@doeixd/tanstackstart-db` turns a small entity schema into a typed application\ndata layer: collections, query helpers, optimistic CRUD actions, reusable views,\nlive React hooks, route loaders, hydration snapshots, and testing utilities.\nIt keeps TanStack DB as the data engine and adds the application-level contracts\nneeded to use that engine consistently across a TanStack Start app.\n\n\u003e **Status**: Pre-release (0.x). The API may change between minor versions.\n\u003e See [GOAL.md](./GOAL.md) for design direction and\n\u003e [docs/](./docs/README.md) for deep-dive topics.\n\n## Why use it?\n\nTanStack DB provides reactive client-side collections, live queries, and\noptimistic mutations. Building an application still requires decisions about\nschema ownership, query naming, view projection, action conventions, SSR\nhydration, route loading, and testing.\n\nThis package makes those decisions once:\n\n- **Define entities once.** Standard Schema-compatible validators drive runtime\n  validation and TypeScript inference.\n- **Get a useful API immediately.** Every entity receives typed `q.*` query\n  helpers and `a.*` CRUD actions.\n- **Keep optimistic writes observable.** Actions return awaitable submissions\n  with transaction, persistence, result, and status state.\n- **Shape data at component boundaries.** Reusable views select fields and\n  materialize declared relationships.\n- **Use the right collection engine per entity.** Start local, connect TanStack\n  Query, persist to `localStorage`, or wrap an existing sync engine such as\n  Electric or PowerSync.\n- **Carry the same model through React and routes.** Live-query hooks, Suspense\n  resources, Router loader builders, confirmed-state hydration, and testing\n  helpers all use the same schema-backed DB.\n- **Describe a page in one place.** DB file routes collect page views, reusable\n  fragments, action aliases, pending state, boundaries, and hydration into one\n  typed TanStack Router contract.\n\n## Positioning\n\nThis package is **TanStack DB with application-level contracts** — not a\nreplacement for TanStack DB and not a Fate-clone. The relationship is:\n\n- **Use raw TanStack DB** when you want the un-augmented engine: live\n  queries, differential dataflow, sync adapters, optimistic mutations,\n  schema validation. Nothing in this package hides those primitives.\n- **Use this package** when you want app-level contracts on top of the\n  same engine: typed schema → query/action generation, view masking,\n  route loaders, action aliases, hydration snapshots, React component\n  builders.\n\nThe two are the same data engine. This package layers **contracts**\n(schema-to-CRUD generation, view masking, action aliases, route loader\ncontracts) on top of TanStack DB. It does not introduce a second\nnormalized cache, transport, or query planner.\n\nEvery abstraction has an escape hatch back to the engine:\n\n- `db.q.raw({ ... })` builds a `DbQuerySpec` from a hand-written\n  native TanStack DB query-builder closure.\n- `db.collections` and `useCollections()` return the typed adapter\n  map; you can read or write rows directly.\n- `useDb()` returns the `StartDb` so React components can reach the\n  generated queries and actions.\n- `nativeCollection(key, engine)` wraps a pre-existing TanStack DB\n  `Collection` (Electric, PowerSync, Query, custom sync engines).\n- `route.useDb()` and `route.useCollections()` expose the route's\n  bound DB to the component for direct access.\n\n## Three levels\n\nThe package is structured so you can adopt it one slice at a time. Most\napps will land in Level 2; the deeper patterns are there when you\nreach for them.\n\n### Level 1: schema to CRUD\n\nDefine entities, get a DB, read and write immediately. No views, no\nroutes, no React. Useful for scripts, server-side rendering helpers,\nor the first hour of an app.\n\n```ts\nimport { defineDbSchema, entity, passthrough } from \"@doeixd/tanstackstart-db/schema\";\nimport { createStartDbFromSchema } from \"@doeixd/tanstackstart-db\";\n\nconst schema = defineDbSchema({\n  entities: {\n    post: entity(passthrough\u003c{ id: string; title: string; likes: number }\u003e(), { key: \"id\" }),\n  },\n});\n\nconst db = createStartDbFromSchema(schema);\n\nawait db.a.post.create({ id: \"post_1\", title: \"Hello\", likes: 0 });\nconst post = await db.q.post.byId(\"post_1\").execute();\nawait db.a.post.patch({ id: \"post_1\", changes: { likes: 1 } });\n```\n\n### Level 2: views, components, and routes\n\nAdd view masking, typed component builders, and Router route\ncontracts. This is the level most apps spend their time in.\n\n```ts\nconst postCard = db.view(\"post\", { id: true, title: true, likes: true });\n\nconst PostCard = db.component(postCard)(({ post }) =\u003e (\n  \u003carticle\u003e\n    \u003ch2\u003e{post.title}\u003c/h2\u003e\n    \u003cp\u003e{post.likes} likes\u003c/p\u003e\n  \u003c/article\u003e\n));\n\nexport const Route = createDbFileRoute(\"/posts/$postId\")\n  .views(({ params, q }) =\u003e ({\n    post: q.post.byId(params.postId).as(postCard).required(),\n  }))\n  .actions(({ a, data }) =\u003e ({\n    like: a.post.patch.with({ id: data.post.id }),\n  }))\n  .build();\n```\n\n### Level 3: custom actions, relationships, adapters, devtools\n\nAdd custom action definitions, schema relationships with native\njoins, collection adapters for sync engines, and a devtools surface\n(planned for v0.2). This is where `extendActions`, `api.one` /\n`api.many`, `localStorageCollection`, `syncCollection`, and the\n`createOptimisticAction` integration live.\n\n```ts\nconst appDb = db.extendActions(({ action, c }) =\u003e ({\n  post: {\n    like: action\u003c{ id: string }, void\u003e({\n      optimistic: ({ input, cache }) =\u003e {\n        cache.post(input.id).patch({ likes: (n: unknown) =\u003e Number(n) + 1 });\n      },\n      run: ({ input, setTransaction }) =\u003e {\n        const result = c.post.update(input.id, (post) =\u003e ({\n          ...post,\n          likes: post.likes + 1,\n        }));\n        setTransaction(result.transaction);\n      },\n    }),\n  },\n}));\n```\n\n## At a glance\n\n| Concern     | What the package provides                                                                                                            |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| Schema      | Standard Schema entities, indexes, and `one` / `many` relationships                                                                  |\n| Queries     | Generated `byId`, `all`, indexed, and relationship helpers; native TanStack DB execution; live subscriptions; raw query escape hatch |\n| Views       | Typed field masks and nested relationship projections, compiled into native selects and joins where possible                         |\n| Actions     | Generated optimistic CRUD, reusable custom actions, pending state, submission history, and rollback behavior                         |\n| Collections | Local TanStack DB collections, Query Collection, `localStorage`, and generic sync-engine adapters                                    |\n| React       | Provider-backed hooks, live queries, Suspense queries, DB-bound component helpers, and status hooks                                  |\n| Routes      | Fluent TanStack Router file-route builders with deferred queries, preload-only queries, route hydration, and cleanup                 |\n| Testing     | Memory-backed DBs, collection seeding, fixtures, action mocks, render helpers, and async utilities                                   |\n\n## Quick start\n\nInstall the package and TanStack DB:\n\n```bash\npnpm add @doeixd/tanstackstart-db @tanstack/db\n```\n\nDefine the entities your application works with. Any Standard Schema-compatible\nlibrary can be used in place of `passthrough`.\n\n```ts\nimport { defineDbSchema, entity, passthrough } from \"@doeixd/tanstackstart-db/schema\";\n\nexport const schema = defineDbSchema({\n  entities: {\n    user: entity(passthrough\u003c{ id: string; name: string }\u003e(), {\n      key: \"id\",\n    }),\n    post: entity(passthrough\u003c{ id: string; authorId: string; title: string; likes: number }\u003e(), {\n      key: \"id\",\n      indexes: [\"authorId\"],\n      relationships: (api) =\u003e ({\n        author: api.one(\"user\", { local: \"authorId\", foreign: \"id\" }),\n      }),\n    }),\n  },\n});\n```\n\nCreate the DB. With no collection configuration, entities use local TanStack DB\ncollections and already support generated actions and queries.\n\n```ts\nimport { createStartDbFromSchema } from \"@doeixd/tanstackstart-db\";\nimport { schema } from \"./schema\";\n\nexport const db = createStartDbFromSchema(schema);\n\nawait db.a.user.create({ id: \"user_1\", name: \"Ada\" });\nawait db.a.post.create({\n  id: \"post_1\",\n  authorId: \"user_1\",\n  title: \"Hello\",\n  likes: 0,\n});\n\nconst post = await db.q.post.byId(\"post_1\").execute();\nconst byAuthor = await db.q.post.byAuthor(\"user_1\").execute();\n```\n\nSchema indexes generate concise helpers such as `authorId -\u003e byAuthor(...)`.\nRelationships generate named query helpers and can also be selected in views.\n\n## Views\n\nViews describe the shape a component or route needs without leaking the full\nentity. Nested views are checked against declared relationships.\n\n```ts\nconst userCard = db.view(\"user\", { id: true, name: true });\nconst postCard = db.view(\"post\", {\n  id: true,\n  title: true,\n  likes: true,\n  author: userCard,\n});\n\nconst post = await db.q.post.byId(\"post_1\").as(postCard).execute();\n// { id, title, likes, author: { id, name } }\n```\n\nSimple projections are compiled into native TanStack DB `select` operations.\nEligible `one` relationships are folded into native joins. Other nested\nrelationships are materialized from their related collections after execution\nand remain reactive for live queries. See\n[`docs/views.md`](./docs/views.md) for the full view contract and\n[`docs/relationships.md`](./docs/relationships.md) for the schema declarations\nthat views depend on.\n\n## Actions and optimistic state\n\nGenerated actions expose `create`, `patch`, `update`, and `delete` for each\nentity. An action call immediately returns a `DbActionSubmission`: a thenable\nobject that can be awaited like a promise while also exposing mutation state.\n\n```ts\nconst submission = db.a.post.patch({\n  id: \"post_1\",\n  changes: { likes: 1 },\n});\n\nsubmission.status; // \"pending\" | \"persisting\" | \"completed\" | \"failed\"\nsubmission.transaction; // native TanStack DB transaction when available\n\nawait submission.persisted;\nconst updated = await submission;\n```\n\nUse the DB tracker to drive granular UI feedback:\n\n```ts\ndb.pending.any();\ndb.pending.action(\"post.patch\", { id: \"post_1\", changes: { likes: 1 } });\ndb.pending.field({ id: \"post_1\" }, \"likes\");\ndb.pending.query(\"post\");\ndb.submissions.latest(\"post.patch\");\n```\n\nApplication-specific actions can extend the generated namespace. The action\npipeline supports optimistic work, authorization, affected-query metadata,\ninvalidation, success/error hooks, and rollback behavior. See\n[`docs/action-aliases.md`](./docs/action-aliases.md) for `.with(...)`,\n`.extend(...)`, and route-level aliasing;\n[`docs/authorization.md`](./docs/authorization.md) for the `authorize` gate\nand `DbAuthError`; and\n[`docs/optimistic-conflict-offline.md`](./docs/optimistic-conflict-offline.md)\nfor the optimistic / conflict / offline failure modes and recovery.\n\n```ts\nconst appDb = db.extendActions(({ action, c, q }) =\u003e ({\n  post: {\n    like: action\u003c{ id: string }, void\u003e({\n      affects: ({ input }) =\u003e [q.post.byId(input.id).field(\"likes\")],\n      run: ({ input, setTransaction }) =\u003e {\n        const result = c.post.update(input.id, (post) =\u003e ({\n          ...post,\n          likes: post.likes + 1,\n        }));\n        setTransaction(result.transaction);\n      },\n    }),\n  },\n}));\n\nawait appDb.a.post.like({ id: \"post_1\" });\n```\n\n## Collection adapters\n\nEach entity can choose its own TanStack DB collection engine. The schema key and\nvalidator are forwarded to adapters so generated actions and direct native\nmutations share the same rules.\n\n### TanStack Query\n\nUse the optional Query Collection entrypoint for server-backed entities:\n\n```ts\nimport { QueryClient } from \"@tanstack/query-core\";\nimport { queryCollection } from \"@doeixd/tanstackstart-db/query-collection\";\n\nconst queryClient = new QueryClient();\n\nconst db = createStartDbFromSchema(schema, {\n  collections: () =\u003e ({\n    post: queryCollection(\"post\", {\n      queryClient,\n      queryKey: [\"posts\"],\n      queryFn: () =\u003e api.posts.list(),\n      mutations: {\n        insert: (rows) =\u003e api.posts.create(rows),\n        update: (updates) =\u003e api.posts.update(updates),\n        delete: (ids) =\u003e api.posts.delete(ids),\n      },\n    }),\n  }),\n});\n```\n\n### Local storage\n\nUse `localStorageCollection` for browser-persisted entities. Its storage API is\nconfigurable, which also makes it straightforward to test.\n\n```ts\nimport { localStorageCollection } from \"@doeixd/tanstackstart-db/local-storage-collection\";\n\nconst db = createStartDbFromSchema(schema, {\n  collections: () =\u003e ({\n    post: localStorageCollection(\"post\", {\n      storageKey: \"app.posts\",\n    }),\n  }),\n});\n```\n\n### Existing sync engines\n\nUse `syncCollection` to wrap an existing TanStack DB `Collection`, including\ncollections created for Electric, PowerSync, or a custom synchronization layer.\n\n```ts\nimport { syncCollection } from \"@doeixd/tanstackstart-db/sync-collection\";\n\nconst db = createStartDbFromSchema(schema, {\n  collections: () =\u003e ({\n    post: syncCollection(\"post\", {\n      engine: electricEngine,\n      key: \"id\",\n    }),\n  }),\n});\n```\n\nFor lower-level integration, `nativeCollection(key, engine, options?)` wraps any\npre-created official TanStack DB collection directly.\n\n## Hydration\n\nSnapshots contain confirmed collection state. Pending optimistic overlays are\nexcluded by default so SSR payloads and client rehydration do not accidentally\npromote unconfirmed writes.\n\n```ts\nimport { dehydrateDb, hydrateDb } from \"@doeixd/tanstackstart-db\";\n\nconst snapshot = dehydrateDb(db);\nhydrateDb(db, snapshot);\n```\n\n### Snapshot strategies\n\n`dehydrateDb` accepts a `snapshot` option to control what state is captured:\n\n- `\"confirmed-only\"` (default) — only rows that have been persisted to the\n  collection's confirmed store. Pending optimistic overlays are excluded.\n- `\"include-pending-for-debug\"` — keep in-flight optimistic overlays in\n  the snapshot. Useful when debugging optimistic-rollback regressions, but\n  never ship this to production SSR or storage because the overlay may\n  never persist.\n\n```ts\n// Default — exclude pending overlays\nconst snapshot = dehydrateDb(db);\n\n// Debug — preserve in-flight optimistic state\nconst debugSnapshot = dehydrateDb(db, { snapshot: \"include-pending-for-debug\" });\n```\n\nThe route builder forwards `defaults.snapshot` to the loader, so a route\ndeclared with `defaults: { hydrate: \"route\", snapshot: \"include-pending-for-debug\" }`\nwill include pending overlays in the SSR payload.\n\n### Hydration strategies\n\n`DbRouteDefaults.hydrate` chooses how the route's SSR loader exposes the\nDB snapshot to the client:\n\n- `\"route\"` (default) — the loader returns `{ data, snapshot }` and the\n  client hydrates the DB on mount. The simplest choice when the root\n  layout is purely structural.\n- `\"root\"` — the loader returns the raw data only. A parent layout is\n  expected to capture the snapshot and hydrate the DB once, so every\n  child route sees the hydrated state without re-sending the snapshot.\n- `\"manual\"` — the loader returns the raw data only. The application is\n  responsible for calling `hydrateDb(db, snapshot)` at the appropriate\n  point (custom route guard, post-auth, `localStorage` reload, etc.).\n\nExternal native adapters are excluded from snapshots by default unless they\nprovide adapter-specific `dehydrate` and `hydrate` behavior. The built-in Query\nCollection, `localStorage`, and generic sync helpers provide suitable defaults.\n\n## React\n\n`@doeixd/tanstackstart-db/react` provides context-backed live-query hooks,\nSuspense resources, status helpers, and component builders.\n\n```tsx\nimport { DbProvider, useDbLiveQuery } from \"@doeixd/tanstackstart-db/react\";\n\nfunction App() {\n  return (\n    \u003cDbProvider db={db}\u003e\n      \u003cPostTitle postId=\"post_1\" /\u003e\n    \u003c/DbProvider\u003e\n  );\n}\n\nfunction PostTitle({ postId }: { postId: string }) {\n  const post = useDbLiveQuery(db.q.post.byId(postId));\n  return \u003ch2\u003e{post?.title}\u003c/h2\u003e;\n}\n```\n\nFor Suspense-driven rendering, use `useDbLiveSuspenseQuery(spec)`. Identical\nquery keys share a retained subscription within the same DB scope and continue\nto emit as the underlying data changes.\n\n### View-bound components\n\nFor components that receive a view-projected row, `db.component(View)` is\nthe flagship builder. The render callback receives the projected row plus\n(optional) `actions`, `pending`, `status`, and `submissions`:\n\n```tsx\nconst PostCard = db.component(postCard)(({ post }) =\u003e (\n  \u003carticle\u003e\n    \u003ch2\u003e{post.title}\u003c/h2\u003e\n    \u003cp\u003e{post.likes} likes\u003c/p\u003e\n  \u003c/article\u003e\n));\n\nconst LikeButton = db\n  .component(postCard)\n  .actions(({ a }) =\u003e ({ like: a.post.patch.with({ id: postCard.id }) }))\n  .render(({ post, actions, pending }) =\u003e (\n    \u003cbutton\n      disabled={pending.field(post, \"likes\")}\n      onClick={() =\u003e actions.like({ changes: { likes: post.likes + 1 } })}\n    \u003e\n      {post.likes}\n    \u003c/button\u003e\n  ));\n```\n\nThe action-aware form threads `actions`, `pending`, `status`, and\n`submissions` through automatically; the component just renders. The DB\nis read from the nearest `DbProvider` at render time, so the same\ncomponent can run against any DB.\n\n### Component-owned queries\n\nFor components that own their query definitions (i.e. don't receive a\nview-projected row), use `createDbComponent(db).props\u003cProps\u003e().views(...).render(...)`:\n\n```tsx\nimport { createDbComponent } from \"@doeixd/tanstackstart-db/react\";\n\nconst PostTitle = createDbComponent(db)\n  .props\u003c{ postId: string }\u003e()\n  .views(({ props, q }) =\u003e ({\n    post: q.post.byId(props.postId).required(),\n  }))\n  .render(({ post }) =\u003e \u003ch2\u003e{post.title}\u003c/h2\u003e);\n```\n\n## Pagination\n\n`@doeixd/tanstackstart-db/pagination` provides cursor-based pagination built on\ntop of TanStack DB collections. Define a paginated collection once, then use\n`useListView` to load pages incrementally with automatic cursor tracking and\nreactive re-renders.\n\n```tsx\nimport { createCollection } from \"@tanstack/db\";\nimport { paginatedCollectionOptions, useListView } from \"@doeixd/tanstackstart-db/pagination\";\n\nconst CommentView = db.view(\"comment\", { id: true, body: true, createdAt: true });\n\nconst comments = createCollection(\n  paginatedCollectionOptions({\n    id: \"post-comments\",\n    getKey: (c) =\u003e c.id,\n    pageSize: 10,\n    cursor: \"id\",\n    direction: \"both\",\n    fetchPage: async ({ after, before, limit }) =\u003e {\n      const params = new URLSearchParams();\n      if (after !== undefined) params.set(\"after\", String(after));\n      if (before !== undefined) params.set(\"before\", String(before));\n      params.set(\"limit\", String(limit));\n      const response = await fetch(`/api/comments?${params}`);\n      return response.json();\n    },\n  }),\n);\n\nfunction PostComments() {\n  const [items, loadNext, loadPrevious, state] = useListView({\n    collection: comments,\n    view: CommentView,\n  });\n\n  return (\n    \u003cdiv\u003e\n      {loadPrevious \u0026\u0026 (\n        \u003cbutton onClick={loadPrevious} disabled={state.isLoadingPrevious}\u003e\n          Load older\n        \u003c/button\u003e\n      )}\n      {items.map((comment) =\u003e (\n        \u003cCommentCard key={comment.id} comment={comment} /\u003e\n      ))}\n      {loadNext \u0026\u0026 (\n        \u003cbutton onClick={loadNext} disabled={state.isLoadingNext}\u003e\n          Load newer\n        \u003c/button\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n```\n\nThe `useListView` hook returns a tuple of `[items, loadNext, loadPrevious, state]`:\n\n- `items` - all currently loaded items projected through the view, reactively updated\n- `loadNext` - function to load the next page, or `undefined` if no more pages or a load is in flight\n- `loadPrevious` - function to load the previous page (only present for `direction: \"backward\"` or `\"both\"`), or `undefined` if at the start or a load is in flight\n- `state` - pagination state including `hasNextPage`, `hasPreviousPage`, `isLoadingNext`, `isLoadingPrevious`, `error`, `totalCount`, `loadedCount`\n\nItems are deduplicated by `getKey`, so calling `loadNextPage` twice with the\nsame cursor is safe. Use `useRefetchPaginated(collection)` to get a stable\nrefetch callback for refresh buttons (clears all loaded data and re-fetches\nthe first page).\n\nThe collection exposes all pagination primitives via `collection.utils`:\n\n- `loadNextPage()` - fetch the next page\n- `loadPreviousPage()` - fetch the previous page (undefined for forward-only)\n- `refetchFirstPage()` - clear all data and re-fetch the first page\n- `subscribe(callback)` - subscribe to pagination state changes\n- `getState()` - get the current pagination state snapshot\n- `getCollection()` - get the underlying collection instance\n\n## Live transport (SSE)\n\n`@doeixd/tanstackstart-db/live-client` and `@doeixd/tanstackstart-db/live-server`\nprovide a complete client/server transport for streaming entity updates over\nServer-Sent Events. The client opens a single EventSource per app; the server\nmultiplexes entity events across connections with a per-connection bounded\nqueue, control messages for subscribe/unsubscribe, and per-record + per-field\nfiltering.\n\n```ts\n// Server (e.g. a TanStack Start API route)\nimport {\n  createLiveEventBus,\n  createLiveSseHandler,\n  createLiveControlHandler,\n} from \"@doeixd/tanstackstart-db/live-server\";\n\nconst bus = createLiveEventBus();\n\nexport const GET = createLiveSseHandler(bus);\nexport const POST = createLiveControlHandler(bus);\n\n// Push a server-side event\nbus.update(\"Post\", \"1\", { changed: [\"likes\"], value: { id: \"1\", likes: 99 } });\n```\n\n```tsx\n// Client\nimport { createCollection } from \"@tanstack/db\";\nimport {\n  createLiveClient,\n  liveCollectionOptions,\n  useLiveCollection,\n} from \"@doeixd/tanstackstart-db/live-client\";\n\nconst live = createLiveClient({ url: \"/api/live\" });\n\nconst posts = createCollection(\n  liveCollectionOptions\u003cPost\u003e({\n    id: \"posts\",\n    entity: \"Post\",\n    live,\n    initialFetch: () =\u003e fetch(\"/api/posts\").then((r) =\u003e r.json()),\n  }),\n);\n\nfunction PostList() {\n  const list = useLiveCollection({ collection: posts, view: PostView });\n  return (\n    \u003cul\u003e\n      {list.map((p) =\u003e (\n        \u003cli key={p.id}\u003e{p.title}\u003c/li\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n```\n\nField-level subscription:\n\n```ts\n// Only receive \"likes\" changes for the Post entity\nlive.subscribe(\n  \"Post\",\n  (event) =\u003e {\n    /* ... */\n  },\n  { fields: [\"likes\"] },\n);\n```\n\nThe wire format is SSE `event: entity.create|update|delete|invalid` with JSON\n`data:` and an optional `id:` (used as `Last-Event-ID` for resumable streams).\nControl messages are POSTed to `${url}/control` (or `controlUrl` if provided)\nas JSON: `{ type: \"subscribe\"|\"unsubscribe\", connectionId, entity, id?, fields? }`.\n\n## DB file routes\n\nThe main application-level value of the React entrypoint is its DB file-route\nbuilder. A DB file route is a typed page contract compiled to an official\nTanStack Router `createFileRoute(path)(options)` route.\n\nInstead of scattering page reads and mutation wiring across loader functions,\ncomponents, and hooks, the route declares:\n\n- the data and views the page needs;\n- reusable fragments shared with other routes;\n- actions the page exposes, including route-bound inputs;\n- pending, submission, refetch, and hydration state;\n- Router search validation, loader dependencies, and boundaries.\n\n```ts\nimport { createDbFileRouteFactory } from \"@doeixd/tanstackstart-db/react\";\n\nconst createDbFileRoute = createDbFileRouteFactory({\n  db,\n  defaults: { hydrate: \"route\" },\n});\n\nexport const Route = createDbFileRoute(\"/posts/$postId\")\n  .views(({ params, q }) =\u003e ({\n    post: q.post.byId(params.postId).as(postCard).required(),\n    recentPosts: q.post.all().defer(),\n  }))\n  .actions(({ a, data }) =\u003e ({\n    renamePost: a.post.patch.with({ id: data.post.id }),\n  }))\n  .build();\n```\n\nUse `.queries(...)` for raw entity results and `.views(...)` when the route is\nexposing masked page contracts. Both methods accept the same typed query specs.\n\n### One route loader request\n\nAll route specs run inside the one Router loader produced by `.build()`.\nIndependent specs in each fragment stage are awaited together with\n`Promise.all`; dependent stages run in order; deferred specs are returned as\npromises; preload-only specs warm collections without becoming component data.\nWhen `hydrate: \"route\"` is enabled, the same loader response also includes a\nconfirmed-state snapshot for client hydration.\n\nThis means one navigation invokes one route loader request. It does **not**\npromise that arbitrary adapter code makes only one backend HTTP call: that is\nowned by the configured TanStack DB collection engine. For example, a Query\nCollection can fetch a collection once and let several route specs query the\nresult locally.\n\n### Route fragments\n\nFragments turn repeated page data into reusable contracts. Use the DB-bound\n`createDbFileRoute.fragment(...)` helper so the callback receives the same typed\n`q` API as the routes that consume it:\n\n```ts\nconst postPageFragment = createDbFileRoute.fragment(({ params, q }) =\u003e ({\n  post: q.post.byId(params.postId).as(postCard).required(),\n  recentPosts: q.post.all().as(postCard).defer(),\n}));\n\nexport const Route = createDbFileRoute(\"/posts/$postId\").views(postPageFragment).build();\n```\n\nRepeated `.queries(...)` and `.views(...)` calls append stages rather than\nreplacing earlier declarations. A later stage can use the data resolved by an\nearlier fragment:\n\n```ts\nexport const Route = createDbFileRoute(\"/posts/$postId\")\n  .views(postPageFragment)\n  .views(({ data, q }) =\u003e ({\n    comments: q.comment.byPost(data.post.id).as(commentCard),\n  }))\n  .build();\n```\n\nUse `composeDbRouteFragments(...)` when the composed contract should be reused\nby several routes:\n\n```ts\nimport { composeDbRouteFragments } from \"@doeixd/tanstackstart-db/react\";\n\nconst postWithComments = composeDbRouteFragments(postPageFragment, ({ data, q }) =\u003e ({\n  comments: q.comment.byPost(data.post.id).as(commentCard),\n}));\n\nexport const Route = createDbFileRoute(\"/posts/$postId\").views(postWithComments).build();\n```\n\nFragment stages still run inside the same Router loader request. Staging exists\nso later query specs can be built from earlier results without creating a\nsecond route loader or moving data dependencies into components.\n\nFor standalone modules that are not attached to a DB-bound factory, the React\nentrypoint also exports `createDbRouteFragment(...)`.\n\n### Route actions\n\nActions define mutation behavior once at the DB layer: input validation,\noptimistic changes, persistence, affected fields and queries, invalidation,\nauthorization, and lifecycle hooks. Routes normally expose those actions,\noptionally rename them, and bind values already known from route data.\n\n```ts\nconst postRoute = createDbFileRoute(\"/posts/$postId\")\n  .views(postPageFragment)\n  .actions(({ a, data }) =\u003e ({\n    renamePost: a.post.patch.with({ id: data.post.id }),\n    deletePost: a.post.delete.with({ id: data.post.id }),\n  }))\n  .component(({ post, actions, pending, submissions }) =\u003e (\n    \u003cPostPage\n      post={post}\n      renaming={pending.action(\"renamePost\")}\n      lastRename={submissions.latest(\"renamePost\")}\n      onRename={(title) =\u003e actions.renamePost({ changes: { title } })}\n      onDelete={() =\u003e actions.deletePost({})}\n    /\u003e\n  ));\n\nexport const Route = postRoute.build();\n```\n\nThe route alias remains connected to the underlying action submission. That\nmeans `pending.action(\"renamePost\")` and `submissions.latest(\"renamePost\")`\nwork even though the canonical action is `post.patch`, and the component only\nsupplies the input that was not already bound from route data.\n\n### Route hooks and loading modes\n\nBefore `.build()`, the builder is also a testable route contract:\n\n```ts\nawait postRoute.load({ params: { postId: \"post_1\" } });\npostRoute.useData();\npostRoute.useActions();\npostRoute.usePending();\npostRoute.useSubmissions();\npostRoute.useStatus();\npostRoute.useQuery(\"post\");\n```\n\nQuery specs can opt into `.required()`, `.defer()`, and `.preloadOnly()`.\nFactory defaults support `hydrate: \"route\"`, `requireViews`, `live: false`,\nand Router `ssr` forwarding. See\n[`docs/pagination.md`](./docs/pagination.md) for the infinite-query runtime,\n`useDbLiveInfiniteQuery` / `useDbLiveInfiniteSuspenseQuery`, and the\nroute-builder's first-page SSR warming; and\n[`docs/query-keys.md`](./docs/query-keys.md) for how query keys compose, how\nthe React resource cache deduplicates, and how `affects(...)` declares\ninvalidation.\n\nDeferred native route queries retain a live subscription after their first\nvalue resolves. Compiled route components release those subscriptions on\nunmount. Loader-only consumers should call `builder.dispose()` when retained\nroute data is no longer needed.\n\n## Server helpers\n\n`@doeixd/tanstackstart-db/server` exports `preloadDbRoute`,\n`dehydrateDbRoute`, and `hydrateDbRoute` for SSR data transfer. It also provides\nthin `createDbServerFn` and `createDbServerHandler` wrappers for environments\nthat already have a DB in scope. Production TanStack Start routes can keep using\nthe framework's own server-function integration.\n\n## Type inference\n\nRuntime schema, view, and query values also carry their projected TypeScript\ntypes:\n\n```ts\nimport type { InferDbQueryResult, InferView } from \"@doeixd/tanstackstart-db\";\nimport type { InferEntity } from \"@doeixd/tanstackstart-db/schema\";\n\ntype Post = InferEntity\u003ctypeof schema.entities.post\u003e;\ntype PostCard = InferView\u003ctypeof postCard\u003e;\ntype PostResult = InferDbQueryResult\u003cReturnType\u003ctypeof db.q.post.byId\u003e\u003e;\n```\n\n## Testing\n\n`@doeixd/tanstackstart-db/testing` provides focused helpers for tests that\nshould exercise the application model without requiring a browser sync engine.\n\n```ts\nimport { createMemoryStartDb, fixture, seedCollections } from \"@doeixd/tanstackstart-db/testing\";\n\nconst db = createMemoryStartDb(schema);\nseedCollections(db, {\n  user: [{ id: \"user_1\", name: \"Ada\" }],\n});\n\nconst userCard = db.view(\"user\", { id: true, name: true });\nconst user = fixture(userCard, { id: \"user_1\", name: \"Ada\" });\n```\n\nThe testing entrypoint also exports `listFixture`, `mockDbAction`,\n`renderDbComponent`, `renderDbRoute`, `waitFor`, and `flushMicrotasks`.\n\nDB route builders can be exercised without mounting a Router:\n\n```ts\nconst { data } = await renderDbRoute(postRoute, {\n  params: { postId: \"post_1\" },\n});\n\nexpect(data.post.title).toBe(\"Hello\");\n```\n\n## Public entrypoints\n\n```txt\n@doeixd/tanstackstart-db\n@doeixd/tanstackstart-db/schema\n@doeixd/tanstackstart-db/react\n@doeixd/tanstackstart-db/server\n@doeixd/tanstackstart-db/testing\n@doeixd/tanstackstart-db/pagination\n@doeixd/tanstackstart-db/live-client\n@doeixd/tanstackstart-db/live-server\n@doeixd/tanstackstart-db/query-collection\n@doeixd/tanstackstart-db/local-storage-collection\n@doeixd/tanstackstart-db/sync-collection\n```\n\nPeer dependency requirements by entrypoint:\n\n- `@tanstack/query-core` and `@tanstack/query-db-collection` for\n  `query-collection`\n- `react` and `@tanstack/react-router` for the root package and `react`\n- `react-dom` for `testing`\n\n## Development\n\nThis repository uses [Vite+](https://viteplus.dev/guide/):\n\n```bash\nvp install\nvp check\nvp test\nvp run build\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoeixd%2Ftanstackstart-db","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoeixd%2Ftanstackstart-db","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoeixd%2Ftanstackstart-db/lists"}