diff --git a/.changeset/config.json b/.changeset/config.json
index 32c538a08..cc95104ae 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -2,7 +2,7 @@
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": [
"@svitejs/changesets-changelog-github-compact",
- { "repo": "TanStack/optimistic" }
+ { "repo": "TanStack/db" }
],
"commit": false,
"access": "public",
diff --git a/.changeset/eighty-ideas-clean.md b/.changeset/eighty-ideas-clean.md
new file mode 100644
index 000000000..c0eb9abe5
--- /dev/null
+++ b/.changeset/eighty-ideas-clean.md
@@ -0,0 +1,5 @@
+---
+"@tanstack/query-db-collection": patch
+---
+
+Improved the type of the queryFn's ctx.meta property of the Query Collection to include the loadSubsetOptions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..8fbf8f760
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,42 @@
+---
+name: π Bug Report
+about: Create a report to help us improve
+title: ""
+labels: ""
+assignees: ""
+---
+
+- [ ] I've validated the bug against the latest version of DB packages
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
+- Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+
+- Device: [e.g. iPhone6]
+- OS: [e.g. iOS8.1]
+- Browser [e.g. stock browser, safari]
+- Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..fa2007e87
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: π€ Feature Requests & Questions
+ url: https://github.com/TanStack/db/discussions
+ about: Please ask and answer questions here.
+ - name: π¬ Community Chat
+ url: https://discord.gg/mQd7egN
+ about: A dedicated discord server hosted by TanStack
+ - name: π¦ TanStack Bluesky
+ url: https://bsky.app/profile/tanstack.com
+ about: Stay up to date with new releases of our libraries
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..cfc170dbe
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,13 @@
+## π― Changes
+
+
+
+## β
Checklist
+
+- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/db/blob/main/CONTRIBUTING.md).
+- [ ] I have tested this code locally with `pnpm test:pr`.
+
+## π Release Impact
+
+- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
+- [ ] This change is docs/CI/dev-only (no release).
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000..aa175e3b7
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "configMigration": true,
+ "extends": [
+ "config:recommended",
+ "group:allNonMajor",
+ "schedule:weekly",
+ ":approveMajorUpdates",
+ ":automergeMinor",
+ ":disablePeerDependencies",
+ ":maintainLockFilesMonthly",
+ ":semanticCommits",
+ ":semanticCommitTypeAll(chore)"
+ ],
+ "ignorePresets": [":ignoreModulesAndTests"],
+ "labels": ["dependencies"],
+ "rangeStrategy": "bump",
+ "postUpdateOptions": ["pnpmDedupe"],
+ "ignoreDeps": ["@types/node", "node", "typescript"]
+}
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index d75eaf0cb..933a6edc4 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -18,12 +18,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v5.0.0
+ with:
+ fetch-depth: 0
- name: Setup Tools
uses: tanstack/config/.github/setup@main
- name: Fix formatting
run: pnpm prettier --ignore-unknown . --check
- name: Apply fixes
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
+ uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
with:
commit-message: "ci: apply automated fixes"
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 000000000..72623ed4d
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,27 @@
+name: Claude Code
+on:
+ issue_comment:
+ types: [created]
+ pull_request_review_comment:
+ types: [created]
+jobs:
+ claude:
+ if: |
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && (github.event.comment.user.login == 'kevin-dp' || github.event.comment.user.login == 'KyleAMathews' || github.event.comment.user.login == 'samwillis')) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && (github.event.comment.user.login == 'kevin-dp' || github.event.comment.user.login == 'KyleAMathews' || github.event.comment.user.login == 'samwillis'))
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ issues: write
+ id-token: write
+ actions: read # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # required for Claude Code
+ - uses: anthropics/claude-code-action@v1
+ with:
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
+ # Responds to @claude mentions in comments
diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml
new file mode 100644
index 000000000..c041168d4
--- /dev/null
+++ b/.github/workflows/docs-sync.yml
@@ -0,0 +1,104 @@
+name: Sync Generated Docs
+
+on:
+ schedule:
+ # Run daily at 2 AM UTC
+ - cron: "0 2 * * *"
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ sync-docs:
+ name: Generate and Sync Docs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+ with:
+ fetch-depth: 0
+
+ - name: Setup Tools
+ uses: tanstack/config/.github/setup@main
+
+ - name: Build Packages
+ run: pnpm run build
+
+ - name: Generate Docs
+ run: pnpm docs:generate
+
+ - name: Check for changes
+ id: check_changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "has_changes=true" >> $GITHUB_OUTPUT
+ echo "Changes detected in generated docs"
+ else
+ echo "has_changes=false" >> $GITHUB_OUTPUT
+ echo "No changes in generated docs"
+ fi
+
+ - name: Configure Git
+ if: steps.check_changes.outputs.has_changes == 'true'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Commit and Push Changes
+ if: steps.check_changes.outputs.has_changes == 'true'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ BRANCH_NAME="docs/auto-generate"
+
+ # Check if branch exists remotely
+ if git ls-remote --exit-code --heads origin $BRANCH_NAME; then
+ echo "Branch exists, checking out and updating"
+ git fetch origin $BRANCH_NAME
+ git checkout $BRANCH_NAME
+ git pull origin $BRANCH_NAME
+ else
+ echo "Creating new branch"
+ git checkout -b $BRANCH_NAME
+ fi
+
+ # Stage and commit changes
+ git add docs/
+ git commit -m "docs: regenerate API documentation
+
+ Auto-generated by daily docs sync workflow"
+
+ # Push changes
+ git push origin $BRANCH_NAME
+
+ - name: Create or Update PR
+ if: steps.check_changes.outputs.has_changes == 'true'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ BRANCH_NAME="docs/auto-generate"
+
+ # Check if PR already exists
+ existing_pr=$(gh pr list --head $BRANCH_NAME --json number --jq '.[0].number')
+
+ if [ -n "$existing_pr" ]; then
+ echo "PR #$existing_pr already exists, it has been updated with the latest changes"
+ gh pr comment $existing_pr --body "Updated with latest generated docs from scheduled workflow run."
+ else
+ echo "Creating new PR"
+ gh pr create \
+ --title "docs: sync generated API documentation" \
+ --body "This PR was automatically created by the daily docs sync workflow.
+
+ The generated API documentation has been updated to reflect the latest changes in the codebase.
+
+ **Generated by**: [Docs Sync Workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
+
+ Please review and merge if the changes look correct." \
+ --head $BRANCH_NAME \
+ --base main
+ fi
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
new file mode 100644
index 000000000..16cadf13a
--- /dev/null
+++ b/.github/workflows/e2e-tests.yml
@@ -0,0 +1,77 @@
+name: E2E Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ e2e-tests:
+ name: Run E2E Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.22.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Start Docker services
+ run: |
+ cd packages/db-collection-e2e/docker
+ docker compose up -d
+ echo "Waiting for services to be healthy..."
+ timeout 60 bash -c 'until docker compose ps | grep -q "healthy"; do sleep 2; done'
+
+ - name: Build packages
+ run: |
+ pnpm --filter @tanstack/db-ivm build
+ pnpm --filter @tanstack/db build
+ pnpm --filter @tanstack/electric-db-collection build
+ pnpm --filter @tanstack/query-db-collection build
+
+ - name: Run Electric E2E tests
+ run: |
+ cd packages/electric-db-collection
+ pnpm test:e2e
+ env:
+ ELECTRIC_URL: http://localhost:3000
+ POSTGRES_HOST: localhost
+ POSTGRES_PORT: 54321
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: e2e_test
+
+ - name: Run Query E2E tests
+ run: |
+ cd packages/query-db-collection
+ pnpm test:e2e
+ env:
+ ELECTRIC_URL: http://localhost:3000
+
+ - name: Stop Docker services
+ if: always()
+ run: |
+ cd packages/db-collection-e2e/docker
+ docker compose down -v
+
+ - name: Upload test results
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results
+ path: packages/db-collection-e2e/junit/
+ retention-days: 7
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 14d56eeaa..4e8e3d55f 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -3,6 +3,9 @@ name: PR
on:
pull_request:
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
cancel-in-progress: true
@@ -20,13 +23,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v5.0.0
with:
fetch-depth: 0
- name: Setup Tools
uses: tanstack/config/.github/setup@main
- name: Get base and head commits for `nx affected`
- uses: nrwl/nx-set-shas@v4.3.0
+ uses: nrwl/nx-set-shas@v4.4.0
with:
main-branch-name: main
- name: Run Checks
@@ -36,7 +39,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v5.0.0
with:
fetch-depth: 0
- name: Setup Tools
@@ -51,18 +54,20 @@ jobs:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
pattern: "./packages/db/dist/**/*.{js,mjs}"
comment-key: "db-package-size"
+ build-script: "build:minified"
- name: Compressed Size Action - React DB Package
uses: preactjs/compressed-size-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
pattern: "./packages/react-db/dist/**/*.{js,mjs}"
comment-key: "react-db-package-size"
+ build-script: "build:minified"
build-example:
name: Build Example Site
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v5.0.0
- name: Setup Tools
uses: tanstack/config/.github/setup@main
- name: Build Packages
@@ -71,3 +76,17 @@ jobs:
run: |
cd examples/react/todo
pnpm build
+ build-starter:
+ name: Build Example Site
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+ - name: Setup Tools
+ uses: tanstack/config/.github/setup@main
+ - name: Build Packages
+ run: pnpm run build
+ - name: Build Starter Site
+ run: |
+ cd examples/react/projects
+ pnpm build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7749bb0b4..e79338f00 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v5.0.0
with:
fetch-depth: 0
- name: Setup Tools
@@ -31,7 +31,8 @@ jobs:
- name: Run Tests
run: pnpm run lint && pnpm run build && pnpm run test
- name: Run Changesets (version or publish)
- uses: changesets/action@v1.4.9
+ id: changesets
+ uses: changesets/action@v1.5.3
with:
version: pnpm run changeset:version
publish: pnpm run changeset:publish
@@ -40,3 +41,36 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: Generate Docs
+ if: steps.changesets.outputs.published == 'true'
+ run: pnpm docs:generate
+ - name: Commit Generated Docs
+ if: steps.changesets.outputs.published == 'true'
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ BRANCH="docs/auto-update-$(date +%s)"
+ git checkout -b "$BRANCH"
+ git add docs/
+ git commit -m "docs: regenerate API documentation"
+ git push origin "$BRANCH"
+
+ gh pr create \
+ --title "docs: regenerate API documentation" \
+ --body "Automated documentation update from release" \
+ --base main \
+ --head "$BRANCH"
+
+ gh pr merge "$BRANCH" --auto --squash
+ else
+ echo "No changes in generated docs"
+ fi
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Comment on PRs about release
+ if: steps.changesets.outputs.published == 'true'
+ uses: tanstack/config/.github/comment-on-release@main
+ with:
+ published-packages: ${{ steps.changesets.outputs.publishedPackages }}
diff --git a/.gitignore b/.gitignore
index 528e53f21..9e71f3da7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -151,3 +151,4 @@ tasks/
.output
.tanstack
.claude
+package-lock.json
diff --git a/.npmrc b/.npmrc
index 84aee8d99..268c392d3 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,3 +1 @@
-link-workspace-packages=true
-prefer-workspace-packages=true
provenance=true
diff --git a/.nvmrc b/.nvmrc
index d5b283a3a..b40402760 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.13.1
+24.8.0
diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs
new file mode 100644
index 000000000..f154da33d
--- /dev/null
+++ b/.pnpmfile.cjs
@@ -0,0 +1,25 @@
+function readPackage(pkg, context) {
+ // Force all @tanstack/db dependencies to resolve to workspace version
+ if (pkg.dependencies && pkg.dependencies["@tanstack/db"]) {
+ pkg.dependencies["@tanstack/db"] = "workspace:*"
+ context.log(`Overriding @tanstack/db dependency in ${pkg.name}`)
+ }
+
+ if (pkg.devDependencies && pkg.devDependencies["@tanstack/db"]) {
+ pkg.devDependencies["@tanstack/db"] = "workspace:*"
+ context.log(`Overriding @tanstack/db devDependency in ${pkg.name}`)
+ }
+
+ if (pkg.peerDependencies && pkg.peerDependencies["@tanstack/db"]) {
+ pkg.peerDependencies["@tanstack/db"] = "workspace:*"
+ context.log(`Overriding @tanstack/db peerDependency in ${pkg.name}`)
+ }
+
+ return pkg
+}
+
+module.exports = {
+ hooks: {
+ readPackage,
+ },
+}
diff --git a/README.md b/README.md
index be627912f..9b12f0958 100644
--- a/README.md
+++ b/README.md
@@ -1,197 +1,130 @@
-# TanStack DB
-
-
-
-**A reactive client store for building super fast apps on sync**
-
-TanStack DB extends TanStack Query with collections, live queries and optimistic mutations that keep your UI reactive, consistent and blazing fast π₯
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Enjoy this library? Try the entire [TanStack](https://tanstack.com), including [TanStack Query](https://tanstack.com/query), [TanStack Store](https://tanstack.com/store), etc.
-
-## π Why TanStack DB?
-
-TanStack DB gives you robust support for real-time sync, live queries and local writes. With no stale data, super fast re-rendering and sub-millisecond cross-collection queries β even for large complex apps.
-
-Built on a TypeScript implementation of differential dataflow ([#](https://github.com/electric-sql/d2ts)), TanStack DB gives you:
-
-- π₯ **a blazing fast query engine**
- for sub-millisecond live queries — even for complex queries with joins and aggregates
-- π― **fine-grained reactivity**
- to minimize component re-rendering
-- πͺ **robust transaction primitives**
- for easy optimistic mutations with sync and lifecycle support
-- π **normalized data**
- to keep your backend simple
-
-TanStack DB is **backend agnostic** and **incrementally adoptable**:
-
-- plug in any backend: sync engines, REST APIs, GraphQL, polling, custom sources
-- builds on [TanStack Store](https://tanstack.com/store), works with and alongside [TanStack Query](https://tanstack.com/query)
-
-## π₯ Usage example
-
-Sync data into collections:
-
-```ts
-import { createCollection, QueryClient } from "@tanstack/react-db"
-import { queryCollectionOptions } from "@tanstack/query-db-collection"
-
-const todoCollection = createCollection(
- queryCollectionOptions({
- queryKey: ["todos"],
- queryFn: async () => fetch("/api/todos"),
- queryClient: new QueryClient(),
- getKey: (item) => item.id,
- schema: todoSchema, // any standard schema
- })
-)
-```
-
-Use live queries in your components:
-
-```tsx
-import { useLiveQuery } from "@tanstack/react-db"
-import { eq } from "@tanstack/db"
-
-const Todos = () => {
- const { data: todos } = useLiveQuery((query) =>
- query
- .from({ todos: todoCollection })
- .where(({ todos }) => eq(todos.completed, false))
- )
-
- return
-}
-```
-
-Apply mutations with local optimistic state:
-
-```tsx
-// Define collection with persistence handlers
-const todoCollection = createCollection({
- id: "todos",
- // ... other config
- onInsert: async ({ transaction }) => {
- const modified = transaction.mutations[0].modified
- await api.todos.create(modified)
- },
-})
-
-// Then use collection operators in your components
-const AddTodo = () => {
- return (
-
- todoCollection.insert({
- id: uuid(),
- text: "π₯ Make app faster",
- completed: false,
- })
- }
- />
- )
-}
-```
-
-## π Docs
+
+
+
+
+
+
+
+
+
+
+
+
+### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
+
-See the [Usage guide](./docs/overview.md) for more details, including how to do:
-
-- real-time sync
-- cross-collection queries
-- fine-grained reactivity
-- different strategies for data loading and handling mutations
-
-There's also an example [React todo app](./examples/react/todo) and usage examples in the [package tests](./packages/db/tests).
-
-## π§± Core concepts
-
-### Collections
-
-- typed sets of objects that can mirror a backend table or be populated with a filtered view or result set, such as `pendingTodos` or `decemberNewTodos`
-- collections are just JavaScript data — load them on demand and define as many as you need
-
-### Live queries
-
-- run reactively against and across collections with support for joins, filters and aggregates
-- powered by differential dataflow: query results update incrementally, not by re-running the whole query
-
-### Transactional mutators
-
-- batch and stage local changes across collections with immediate application of local optimistic updates
-- sync transactions to the backend with automatic rollbacks and management of optimistic state
-
-## π¦ Collection Types
-
-TanStack DB provides several collection types to support different backend integrations:
-
-- **`@tanstack/db`** - Core collection functionality with local-only and local-storage collections for offline-first applications
-- **`@tanstack/query-db-collection`** - Collections backed by [TanStack Query](https://tanstack.com/query) for REST APIs and GraphQL endpoints
-- **`@tanstack/electric-db-collection`** - Real-time sync collections powered by [ElectricSQL](https://electric-sql.com) for live database synchronization
-- **`@tanstack/trailbase-db-collection`** - Collections for [TrailBase](https://trailbase.io) backend integration
-
-## Framework integrations
-
-TanStack DB integrates with React & Vue with more on the way!
-
-- **`@tanstack/react-db`** - React hooks and components for using TanStack DB collections in React applications
-- **`@tanstack/vue-db`** - Vue composables for using TanStack DB collections in Vue applications
+# TanStack DB
-## π§ Install
+> Tanstack DB is currently in BETA. See [the release post](https://tanstack.com/blog/tanstack-db-0.1-the-embedded-client-database-for-tanstack-query) for more details.
-```bash
-npm install @tanstack/react-db
-# Optional: for specific collection types
-npm install @tanstack/electric-db-collection @tanstack/query-db-collection
-```
+The reactive client store for your API.
-Other framework integrations are in progress.
+TanStack DB solves the problems of building fast, modern apps, helping you:
-## β FAQ
+- Avoid endpoint sprawl and network waterfalls by loading data into normalized collections
+- Optimise client performance with sub-millisecond live queries and real-time reactivity
+- Take the network off the interaction path with instant optimistic writes
-**How is this different from TanStack Query?**
-TanStack DB builds _on top of_ TanStack Query. Use Query to fetch data; use DB to manage reactive local collections and mutations. They complement each other.
+Data loading is optimized. Interactions feel instantaneous. Your backend stays simple and your app stays blazing fast. No matter how much data you load.
-**Do I need a sync engine like ElectricSQL?**
-No. TanStack DB _is_ designed to work with sync engines like [Electric](https://electric-sql.com) but _also_ works with any backend: polling APIs, GraphQL, REST, or custom sync logic.
+Read the docs β
+
-**What is a Collection? Is it like a DB table?**
-Kind of. Collections are typed sets of objects, but they can also be filtered views or custom groupings. They're just JavaScript structures that you define and manage.
+## Get Involved
-**Is this an ORM? Do queries hit my backend?**
-No. TanStack DB is not an ORM. Queries run entirely in the client against local collections. The framework provides strong primitives to manage how data is loaded and synced.
+- We welcome issues and pull requests!
+- Participate in [GitHub discussions](https://github.com/TanStack/db/discussions)
+- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
+- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions
## Partners
+
-## Status
-
-Tanstack DB is currently an early preview release in alpha. It's still under active development. There will be bugs and the APIs are still liable to change.
-
-## Contributing
-
-View the contributing guidelines [here](https://github.com/TanStack/query/blob/main/CONTRIBUTING.md).
-
-### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
+
+
+
+We're looking for TanStack DB Partners to join our mission! Partner with us to push the boundaries of TanStack DB and build amazing things together.
+
+
LET'S CHAT
+
+
+## Explore the TanStack Ecosystem
+
+- TanStack Config β Tooling for JS/TS packages
+- TanStack DevTools β Unified devtools panel
+- TanStack Form β Typeβsafe form state
+- TanStack Pacer β Debouncing, throttling, batching
+- TanStack Query β Async state & caching
+- TanStack Ranger β Range & slider primitives
+- TanStack Router β Typeβsafe routing, caching & URL state
+- TanStack Start β Fullβstack SSR & streaming
+- TanStack Store β Reactive data store
+- TanStack Table β Headless datagrids
+- TanStack Virtual β Virtualized rendering
+
+β¦ and more at TanStack.com Β»
+v>
diff --git a/docs/collections/electric-collection.md b/docs/collections/electric-collection.md
new file mode 100644
index 000000000..228098762
--- /dev/null
+++ b/docs/collections/electric-collection.md
@@ -0,0 +1,446 @@
+---
+title: Electric Collection
+---
+
+# Electric Collection
+
+Electric collections provide seamless integration between TanStack DB and ElectricSQL, enabling real-time data synchronization with your Postgres database through Electric's sync engine.
+
+## Overview
+
+The `@tanstack/electric-db-collection` package allows you to create collections that:
+- Automatically sync data from Postgres via Electric shapes
+- Support optimistic updates with transaction matching and automatic rollback on errors
+- Handle persistence through customizable mutation handlers
+
+## Installation
+
+```bash
+npm install @tanstack/electric-db-collection @tanstack/react-db
+```
+
+## Basic Usage
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { electricCollectionOptions } from '@tanstack/electric-db-collection'
+
+const todosCollection = createCollection(
+ electricCollectionOptions({
+ shapeOptions: {
+ url: '/api/todos',
+ },
+ getKey: (item) => item.id,
+ })
+)
+```
+
+## Configuration Options
+
+The `electricCollectionOptions` function accepts the following options:
+
+### Required Options
+
+- `shapeOptions`: Configuration for the ElectricSQL ShapeStream
+ - `url`: The URL of your proxy to Electric
+
+- `getKey`: Function to extract the unique key from an item
+
+### Optional
+
+- `id`: Unique identifier for the collection
+- `schema`: Schema for validating items. Any Standard Schema compatible schema
+- `sync`: Custom sync configuration
+
+### Persistence Handlers
+
+Handlers are called before mutations to persist changes to your backend:
+
+- `onInsert`: Handler called before insert operations
+- `onUpdate`: Handler called before update operations
+- `onDelete`: Handler called before delete operations
+
+Each handler should return `{ txid }` to wait for synchronization. For cases where your API can not return txids, use the `awaitMatch` utility function.
+
+## Persistence Handlers & Synchronization
+
+Handlers persist mutations to the backend and wait for Electric to sync the changes back. This prevents UI glitches where optimistic updates would be removed and then re-added. TanStack DB blocks sync data until the mutation is confirmed, ensuring smooth user experience.
+
+### 1. Using Txid (Recommended)
+
+The recommended approach uses PostgreSQL transaction IDs (txids) for precise matching. The backend returns a txid, and the client waits for that specific txid to appear in the Electric stream.
+
+```typescript
+const todosCollection = createCollection(
+ electricCollectionOptions({
+ id: 'todos',
+ schema: todoSchema,
+ getKey: (item) => item.id,
+ shapeOptions: {
+ url: '/api/todos',
+ params: { table: 'todos' },
+ },
+
+ onInsert: async ({ transaction }) => {
+ const newItem = transaction.mutations[0].modified
+ const response = await api.todos.create(newItem)
+
+ // Return txid to wait for sync
+ return { txid: response.txid }
+ },
+
+ onUpdate: async ({ transaction }) => {
+ const { original, changes } = transaction.mutations[0]
+ const response = await api.todos.update({
+ where: { id: original.id },
+ data: changes
+ })
+
+ return { txid: response.txid }
+ }
+ })
+)
+```
+
+### 2. Using Custom Match Functions
+
+For cases where txids aren't available, use the `awaitMatch` utility function to wait for synchronization with custom matching logic:
+
+```typescript
+import { isChangeMessage } from '@tanstack/electric-db-collection'
+
+const todosCollection = createCollection(
+ electricCollectionOptions({
+ id: 'todos',
+ getKey: (item) => item.id,
+ shapeOptions: {
+ url: '/api/todos',
+ params: { table: 'todos' },
+ },
+
+ onInsert: async ({ transaction, collection }) => {
+ const newItem = transaction.mutations[0].modified
+ await api.todos.create(newItem)
+
+ // Use awaitMatch utility for custom matching
+ await collection.utils.awaitMatch(
+ (message) => {
+ return isChangeMessage(message) &&
+ message.headers.operation === 'insert' &&
+ message.value.text === newItem.text
+ },
+ 5000 // timeout in ms (optional, defaults to 3000)
+ )
+ }
+ })
+)
+```
+
+### 3. Using Simple Timeout
+
+For quick prototyping or when you're confident about timing, you can use a simple timeout. This is crude but works as almost always the data will be synced back in under 2 seconds:
+
+```typescript
+const todosCollection = createCollection(
+ electricCollectionOptions({
+ id: 'todos',
+ getKey: (item) => item.id,
+ shapeOptions: {
+ url: '/api/todos',
+ params: { table: 'todos' },
+ },
+
+ onInsert: async ({ transaction }) => {
+ const newItem = transaction.mutations[0].modified
+ await api.todos.create(newItem)
+
+ // Simple timeout approach
+ await new Promise(resolve => setTimeout(resolve, 2000))
+ }
+ })
+)
+```
+
+On the backend, you can extract the `txid` for a transaction by querying Postgres directly.
+
+```ts
+async function generateTxId(tx) {
+ // The ::xid cast strips off the epoch, giving you the raw 32-bit value
+ // that matches what PostgreSQL sends in logical replication streams
+ // (and then exposed through Electric which we'll match against
+ // in the client).
+ const result = await tx.execute(
+ sql`SELECT pg_current_xact_id()::xid::text as txid`
+ )
+ const txid = result.rows[0]?.txid
+
+ if (txid === undefined) {
+ throw new Error(`Failed to get transaction ID`)
+ }
+
+ return parseInt(txid as string, 10)
+}
+```
+
+### Electric Proxy Example
+
+Electric is typically deployed behind a proxy server that handles shape configuration, authentication and authorization. This provides better security and allows you to control what data users can access without exposing Electric to the client.
+
+
+Here is an example proxy implementation using TanStack Starter:
+
+```js
+import { createServerFileRoute } from "@tanstack/react-start/server"
+import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"
+
+// Electric URL
+const baseUrl = 'http://.../v1/shape'
+
+const serve = async ({ request }: { request: Request }) => {
+ // ...check user authorization
+ const url = new URL(request.url)
+ const originUrl = new URL(baseUrl)
+
+ // passthrough parameters from electric client
+ url.searchParams.forEach((value, key) => {
+ if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
+ originUrl.searchParams.set(key, value)
+ }
+ })
+
+ // set shape parameters
+ // full spec: https://github.com/electric-sql/electric/blob/main/website/electric-api.yaml
+ originUrl.searchParams.set("table", "todos")
+ // Where clause to filter rows in the table (optional).
+ // originUrl.searchParams.set("where", "completed = true")
+
+ // Select the columns to sync (optional)
+ // originUrl.searchParams.set("columns", "id,text,completed")
+
+ const response = await fetch(originUrl)
+ const headers = new Headers(response.headers)
+ headers.delete("content-encoding")
+ headers.delete("content-length")
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ })
+}
+
+export const ServerRoute = createServerFileRoute("/api/todos").methods({
+ GET: serve,
+})
+```
+
+## Optimistic Updates with Explicit Transactions
+
+For more advanced use cases, you can create custom actions that can do multiple mutations across collections transactionally. You can use the utility methods to wait for synchronization with different strategies:
+
+### Using Txid Strategy
+
+```typescript
+const addTodoAction = createOptimisticAction({
+ onMutate: ({ text }) => {
+ // optimistically insert with a temporary ID
+ const tempId = crypto.randomUUID()
+ todosCollection.insert({
+ id: tempId,
+ text,
+ completed: false,
+ created_at: new Date(),
+ })
+
+ // ... mutate other collections
+ },
+
+ mutationFn: async ({ text }) => {
+ const response = await api.todos.create({
+ data: { text, completed: false }
+ })
+
+ // Wait for the specific txid
+ await todosCollection.utils.awaitTxId(response.txid)
+ }
+})
+```
+
+### Using Custom Match Function
+
+```typescript
+import { isChangeMessage } from '@tanstack/electric-db-collection'
+
+const addTodoAction = createOptimisticAction({
+ onMutate: ({ text }) => {
+ const tempId = crypto.randomUUID()
+ todosCollection.insert({
+ id: tempId,
+ text,
+ completed: false,
+ created_at: new Date(),
+ })
+ },
+
+ mutationFn: async ({ text }) => {
+ await api.todos.create({
+ data: { text, completed: false }
+ })
+
+ // Wait for matching message
+ await todosCollection.utils.awaitMatch(
+ (message) => {
+ return isChangeMessage(message) &&
+ message.headers.operation === 'insert' &&
+ message.value.text === text
+ }
+ )
+ }
+})
+```
+
+## Utility Methods
+
+The collection provides these utility methods via `collection.utils`:
+
+### `awaitTxId(txid, timeout?)`
+
+Manually wait for a specific transaction ID to be synchronized:
+
+```typescript
+// Wait for specific txid
+await todosCollection.utils.awaitTxId(12345)
+
+// With custom timeout (default is 30 seconds)
+await todosCollection.utils.awaitTxId(12345, 10000)
+```
+
+This is useful when you need to ensure a mutation has been synchronized before proceeding with other operations.
+
+### `awaitMatch(matchFn, timeout?)`
+
+Manually wait for a custom match function to find a matching message:
+
+```typescript
+import { isChangeMessage } from '@tanstack/electric-db-collection'
+
+// Wait for a specific message pattern
+await todosCollection.utils.awaitMatch(
+ (message) => {
+ return isChangeMessage(message) &&
+ message.headers.operation === 'insert' &&
+ message.value.text === 'New Todo'
+ },
+ 5000 // timeout in ms
+)
+```
+
+### Helper Functions
+
+The package exports helper functions for use in custom match functions:
+
+- `isChangeMessage(message)`: Check if a message is a data change (insert/update/delete)
+- `isControlMessage(message)`: Check if a message is a control message (up-to-date, must-refetch)
+
+```typescript
+import { isChangeMessage, isControlMessage } from '@tanstack/electric-db-collection'
+
+// Use in custom match functions
+const matchFn = (message) => {
+ if (isChangeMessage(message)) {
+ return message.headers.operation === 'insert'
+ }
+ return false
+}
+```
+
+## Debugging
+
+### Common Issue: awaitTxId Stalls or Times Out
+
+A frequent issue developers encounter is that `awaitTxId` (or the transaction's `isPersisted.promise`) stalls indefinitely, eventually timing out with no error messages. The data persists correctly to the database, but the optimistic mutation never resolves.
+
+**Root Cause:** This happens when the transaction ID (txid) returned from your API doesn't match the actual transaction ID of the mutation in Postgres. This mismatch occurs when you query `pg_current_xact_id()` **outside** the same transaction that performs the mutation.
+
+### Enable Debug Logging
+
+To diagnose txid issues, enable debug logging in your browser console:
+
+```javascript
+localStorage.debug = 'ts/db:electric'
+```
+
+This will show you when mutations start waiting for txids and when txids arrive from Electric's sync stream.
+
+This is powered by the [debug](https://www.npmjs.com/package/debug) package.
+
+**When txids DON'T match (common bug):**
+```
+ts/db:electric awaitTxId called with txid 124
+ts/db:electric new txids synced from pg [123]
+// Stalls forever - 124 never arrives!
+```
+
+In this example, the mutation happened in transaction 123, but you queried `pg_current_xact_id()` in a separate transaction (124) that ran after the mutation. The client waits for 124 which will never arrive.
+
+**When txids DO match (correct):**
+```
+ts/db:electric awaitTxId called with txid 123
+ts/db:electric new txids synced from pg [123]
+ts/db:electric awaitTxId found match for txid 123
+// Resolves immediately!
+```
+
+### The Solution: Query txid Inside the Transaction
+
+You **must** call `pg_current_xact_id()` inside the same transaction as your mutation:
+
+**β Wrong - txid queried outside transaction:**
+```typescript
+// DON'T DO THIS
+async function createTodo(data) {
+ const txid = await generateTxId(sql) // Wrong: separate transaction
+
+ await sql.begin(async (tx) => {
+ await tx`INSERT INTO todos ${tx(data)}`
+ })
+
+ return { txid } // This txid won't match!
+}
+```
+
+**β
Correct - txid queried inside transaction:**
+```typescript
+// DO THIS
+async function createTodo(data) {
+ let txid!: Txid
+
+ const result = await sql.begin(async (tx) => {
+ // Call generateTxId INSIDE the transaction
+ txid = await generateTxId(tx)
+
+ const [todo] = await tx`
+ INSERT INTO todos ${tx(data)}
+ RETURNING *
+ `
+ return todo
+ })
+
+ return { todo: result, txid } // txid matches the mutation
+}
+
+async function generateTxId(tx: any): Promise {
+ const result = await tx`SELECT pg_current_xact_id()::xid::text as txid`
+ const txid = result[0]?.txid
+
+ if (txid === undefined) {
+ throw new Error(`Failed to get transaction ID`)
+ }
+
+ return parseInt(txid, 10)
+}
+```
+
+See working examples in:
+- `examples/react/todo/src/routes/api/todos.ts`
+- `examples/react/todo/src/api/server.ts`
diff --git a/docs/collections/local-only-collection.md b/docs/collections/local-only-collection.md
new file mode 100644
index 000000000..a555ba9e5
--- /dev/null
+++ b/docs/collections/local-only-collection.md
@@ -0,0 +1,324 @@
+---
+title: LocalOnly Collection
+---
+
+# LocalOnly Collection
+
+LocalOnly collections are designed for in-memory client data or UI state that doesn't need to persist across browser sessions or sync across tabs.
+
+## Overview
+
+The `localOnlyCollectionOptions` allows you to create collections that:
+- Store data only in memory (no persistence)
+- Support optimistic updates with automatic rollback on errors
+- Provide optional initial data
+- Work perfectly for temporary UI state and session-only data
+- Automatically manage the transition from optimistic to confirmed state
+
+## Installation
+
+LocalOnly collections are included in the core TanStack DB package:
+
+```bash
+npm install @tanstack/react-db
+```
+
+## Basic Usage
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { localOnlyCollectionOptions } from '@tanstack/react-db'
+
+const uiStateCollection = createCollection(
+ localOnlyCollectionOptions({
+ id: 'ui-state',
+ getKey: (item) => item.id,
+ })
+)
+```
+
+### Direct Local Mutations
+
+**Important:** LocalOnly collections work differently than server-synced collections. With LocalOnly collections, you **directly mutate state** by calling methods like `collection.insert()`, `collection.update()`, and `collection.delete()` β that's all you need to do. The changes are immediately applied to your local in-memory data.
+
+This is different from collections that sync with a server (like Query Collection), where mutation handlers send data to a backend. With LocalOnly collections, everything stays local:
+
+```typescript
+// Just call the methods directly - no server sync involved
+uiStateCollection.insert({ id: 'theme', mode: 'dark' })
+uiStateCollection.update('theme', (draft) => { draft.mode = 'light' })
+uiStateCollection.delete('theme')
+```
+
+## Configuration Options
+
+The `localOnlyCollectionOptions` function accepts the following options:
+
+### Required Options
+
+- `id`: Unique identifier for the collection
+- `getKey`: Function to extract the unique key from an item
+
+### Optional Options
+
+- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation
+- `initialData`: Array of items to populate the collection with on creation
+- `onInsert`: Optional handler function called before confirming inserts
+- `onUpdate`: Optional handler function called before confirming updates
+- `onDelete`: Optional handler function called before confirming deletes
+
+## Initial Data
+
+Populate the collection with initial data on creation:
+
+```typescript
+const uiStateCollection = createCollection(
+ localOnlyCollectionOptions({
+ id: 'ui-state',
+ getKey: (item) => item.id,
+ initialData: [
+ { id: 'sidebar', isOpen: false },
+ { id: 'theme', mode: 'light' },
+ { id: 'modal', visible: false },
+ ],
+ })
+)
+```
+
+## Mutation Handlers
+
+Mutation handlers are **completely optional**. When provided, they are called before the optimistic state is confirmed:
+
+```typescript
+const tempDataCollection = createCollection(
+ localOnlyCollectionOptions({
+ id: 'temp-data',
+ getKey: (item) => item.id,
+ onInsert: async ({ transaction }) => {
+ // Custom logic before confirming the insert
+ console.log('Inserting:', transaction.mutations[0].modified)
+ },
+ onUpdate: async ({ transaction }) => {
+ // Custom logic before confirming the update
+ const { original, modified } = transaction.mutations[0]
+ console.log('Updating from', original, 'to', modified)
+ },
+ onDelete: async ({ transaction }) => {
+ // Custom logic before confirming the delete
+ console.log('Deleting:', transaction.mutations[0].original)
+ },
+ })
+)
+```
+
+## Manual Transactions
+
+When using LocalOnly collections with manual transactions (created via `createTransaction`), you must call `utils.acceptMutations()` to persist the changes:
+
+```typescript
+import { createTransaction } from '@tanstack/react-db'
+
+const localData = createCollection(
+ localOnlyCollectionOptions({
+ id: 'form-draft',
+ getKey: (item) => item.id,
+ })
+)
+
+const serverCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ['items'],
+ queryFn: async () => api.items.getAll(),
+ getKey: (item) => item.id,
+ onInsert: async ({ transaction }) => {
+ await api.items.create(transaction.mutations[0].modified)
+ },
+ })
+)
+
+const tx = createTransaction({
+ mutationFn: async ({ transaction }) => {
+ // Handle server collection mutations explicitly in mutationFn
+ await Promise.all(
+ transaction.mutations
+ .filter((m) => m.collection === serverCollection)
+ .map((m) => api.items.create(m.modified))
+ )
+
+ // After server mutations succeed, accept local collection mutations
+ localData.utils.acceptMutations(transaction)
+ },
+})
+
+// Apply mutations to both collections in one transaction
+tx.mutate(() => {
+ localData.insert({ id: 'draft-1', data: '...' })
+ serverCollection.insert({ id: '1', name: 'Item' })
+})
+
+await tx.commit()
+```
+
+## Complete Example: Modal State Management
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { localOnlyCollectionOptions } from '@tanstack/react-db'
+import { useLiveQuery } from '@tanstack/react-db'
+import { z } from 'zod'
+
+// Define schema
+const modalStateSchema = z.object({
+ id: z.string(),
+ isOpen: z.boolean(),
+ data: z.any().optional(),
+})
+
+type ModalState = z.infer
+
+// Create collection
+export const modalStateCollection = createCollection(
+ localOnlyCollectionOptions({
+ id: 'modal-state',
+ getKey: (item) => item.id,
+ schema: modalStateSchema,
+ initialData: [
+ { id: 'user-profile', isOpen: false },
+ { id: 'settings', isOpen: false },
+ { id: 'confirm-delete', isOpen: false },
+ ],
+ })
+)
+
+// Use in component
+function UserProfileModal() {
+ const { data: modals } = useLiveQuery((q) =>
+ q.from({ modal: modalStateCollection })
+ .where(({ modal }) => modal.id === 'user-profile')
+ )
+
+ const modalState = modals[0]
+
+ const openModal = (data?: any) => {
+ modalStateCollection.update('user-profile', (draft) => {
+ draft.isOpen = true
+ draft.data = data
+ })
+ }
+
+ const closeModal = () => {
+ modalStateCollection.update('user-profile', (draft) => {
+ draft.isOpen = false
+ draft.data = undefined
+ })
+ }
+
+ if (!modalState?.isOpen) return null
+
+ return (
+
+
User Profile
+
{JSON.stringify(modalState.data, null, 2)}
+
Close
+
+ )
+}
+```
+
+## Complete Example: Form Draft State
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { localOnlyCollectionOptions } from '@tanstack/react-db'
+import { useLiveQuery } from '@tanstack/react-db'
+
+type FormDraft = {
+ id: string
+ formData: Record
+ lastModified: Date
+}
+
+// Create collection for form drafts
+export const formDraftsCollection = createCollection(
+ localOnlyCollectionOptions({
+ id: 'form-drafts',
+ getKey: (item) => item.id,
+ })
+)
+
+// Use in component
+function CreatePostForm() {
+ const { data: drafts } = useLiveQuery((q) =>
+ q.from({ draft: formDraftsCollection })
+ .where(({ draft }) => draft.id === 'new-post')
+ )
+
+ const currentDraft = drafts[0]
+
+ const updateDraft = (field: string, value: any) => {
+ if (currentDraft) {
+ formDraftsCollection.update('new-post', (draft) => {
+ draft.formData[field] = value
+ draft.lastModified = new Date()
+ })
+ } else {
+ formDraftsCollection.insert({
+ id: 'new-post',
+ formData: { [field]: value },
+ lastModified: new Date(),
+ })
+ }
+ }
+
+ const clearDraft = () => {
+ if (currentDraft) {
+ formDraftsCollection.delete('new-post')
+ }
+ }
+
+ const submitForm = async () => {
+ if (!currentDraft) return
+
+ await api.posts.create(currentDraft.formData)
+ clearDraft()
+ }
+
+ return (
+
+ )
+}
+```
+
+## Use Cases
+
+LocalOnly collections are perfect for:
+- Temporary UI state (modals, sidebars, tooltips)
+- Form draft data during the current session
+- Client-side computed or derived data
+- Wizard/multi-step form state
+- Temporary filters or search state
+- In-memory caches
+
+## Comparison with LocalStorageCollection
+
+| Feature | LocalOnly | LocalStorage |
+|---------|-----------|--------------|
+| Persistence | None (in-memory only) | localStorage |
+| Cross-tab sync | No | Yes |
+| Survives page reload | No | Yes |
+| Performance | Fastest | Fast |
+| Size limits | Memory limits | ~5-10MB |
+| Best for | Temporary UI state | User preferences |
+
+## Learn More
+
+- [Optimistic Mutations](../guides/mutations.md)
+- [Live Queries](../guides/live-queries.md)
+- [LocalStorage Collection](./local-storage-collection.md)
diff --git a/docs/collections/local-storage-collection.md b/docs/collections/local-storage-collection.md
new file mode 100644
index 000000000..3033c3bbd
--- /dev/null
+++ b/docs/collections/local-storage-collection.md
@@ -0,0 +1,312 @@
+---
+title: LocalStorage Collection
+---
+
+# LocalStorage Collection
+
+LocalStorage collections store small amounts of local-only state that persists across browser sessions and syncs across browser tabs in real-time.
+
+## Overview
+
+The `localStorageCollectionOptions` allows you to create collections that:
+- Persist data to localStorage (or sessionStorage)
+- Automatically sync across browser tabs using storage events
+- Support optimistic updates with automatic rollback on errors
+- Store all data under a single localStorage key
+- Work with any storage API that matches the localStorage interface
+
+## Installation
+
+LocalStorage collections are included in the core TanStack DB package:
+
+```bash
+npm install @tanstack/react-db
+```
+
+## Basic Usage
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { localStorageCollectionOptions } from '@tanstack/react-db'
+
+const userPreferencesCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'user-preferences',
+ storageKey: 'app-user-prefs',
+ getKey: (item) => item.id,
+ })
+)
+```
+
+### Direct Local Mutations
+
+**Important:** LocalStorage collections work differently than server-synced collections. With LocalStorage collections, you **directly mutate state** by calling methods like `collection.insert()`, `collection.update()`, and `collection.delete()` β that's all you need to do. The changes are immediately applied to your local data and automatically persisted to localStorage.
+
+This is different from collections that sync with a server (like Query Collection), where mutation handlers send data to a backend. With LocalStorage collections, everything stays local:
+
+```typescript
+// Just call the methods directly - automatically persisted to localStorage
+userPreferencesCollection.insert({ id: 'theme', mode: 'dark' })
+userPreferencesCollection.update('theme', (draft) => { draft.mode = 'light' })
+userPreferencesCollection.delete('theme')
+```
+
+## Configuration Options
+
+The `localStorageCollectionOptions` function accepts the following options:
+
+### Required Options
+
+- `id`: Unique identifier for the collection
+- `storageKey`: The localStorage key where all collection data is stored
+- `getKey`: Function to extract the unique key from an item
+
+### Optional Options
+
+- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation
+- `storage`: Custom storage implementation (defaults to `localStorage`). Can be `sessionStorage` or any object with the localStorage API
+- `storageEventApi`: Event API for subscribing to storage events (defaults to `window`). Enables custom cross-tab, cross-window, or cross-process synchronization
+- `onInsert`: Optional handler function called when items are inserted
+- `onUpdate`: Optional handler function called when items are updated
+- `onDelete`: Optional handler function called when items are deleted
+
+## Cross-Tab Synchronization
+
+LocalStorage collections automatically sync across browser tabs in real-time:
+
+```typescript
+const settingsCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'settings',
+ storageKey: 'app-settings',
+ getKey: (item) => item.id,
+ })
+)
+
+// Changes in one tab are automatically reflected in all other tabs
+// This works automatically via storage events
+```
+
+## Using SessionStorage
+
+You can use `sessionStorage` instead of `localStorage` for session-only persistence:
+
+```typescript
+const sessionCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'session-data',
+ storageKey: 'session-key',
+ storage: sessionStorage, // Use sessionStorage instead
+ getKey: (item) => item.id,
+ })
+)
+```
+
+## Custom Storage Backend
+
+Provide any storage implementation that matches the localStorage API:
+
+```typescript
+// Example: Custom storage wrapper with encryption
+const encryptedStorage = {
+ getItem(key: string) {
+ const encrypted = localStorage.getItem(key)
+ return encrypted ? decrypt(encrypted) : null
+ },
+ setItem(key: string, value: string) {
+ localStorage.setItem(key, encrypt(value))
+ },
+ removeItem(key: string) {
+ localStorage.removeItem(key)
+ },
+}
+
+const secureCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'secure-data',
+ storageKey: 'encrypted-key',
+ storage: encryptedStorage,
+ getKey: (item) => item.id,
+ })
+)
+```
+
+### Cross-Tab Sync with Custom Storage
+
+The `storageEventApi` option (defaults to `window`) allows the collection to subscribe to storage events for cross-tab synchronization. A custom storage implementation can provide this API to enable custom cross-tab, cross-window, or cross-process sync:
+
+```typescript
+// Example: Custom storage event API for cross-process sync
+const customStorageEventApi = {
+ addEventListener(event: string, handler: (e: StorageEvent) => void) {
+ // Custom event subscription logic
+ // Could be IPC, WebSocket, or any other mechanism
+ myCustomEventBus.on('storage-change', handler)
+ },
+ removeEventListener(event: string, handler: (e: StorageEvent) => void) {
+ myCustomEventBus.off('storage-change', handler)
+ },
+}
+
+const syncedCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'synced-data',
+ storageKey: 'data-key',
+ storage: customStorage,
+ storageEventApi: customStorageEventApi, // Custom event API
+ getKey: (item) => item.id,
+ })
+)
+```
+
+This enables synchronization across different contexts beyond just browser tabs, such as:
+- Cross-process communication in Electron apps
+- WebSocket-based sync across multiple browser windows
+- Custom IPC mechanisms in desktop applications
+
+## Mutation Handlers
+
+Mutation handlers are **completely optional**. Data will persist to localStorage whether or not you provide handlers:
+
+```typescript
+const preferencesCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'preferences',
+ storageKey: 'user-prefs',
+ getKey: (item) => item.id,
+ // Optional: Add custom logic when preferences are updated
+ onUpdate: async ({ transaction }) => {
+ const { modified } = transaction.mutations[0]
+ console.log('Preference updated:', modified)
+ // Maybe send analytics or trigger other side effects
+ },
+ })
+)
+```
+
+## Manual Transactions
+
+When using LocalStorage collections with manual transactions (created via `createTransaction`), you must call `utils.acceptMutations()` to persist the changes:
+
+```typescript
+import { createTransaction } from '@tanstack/react-db'
+
+const localData = createCollection(
+ localStorageCollectionOptions({
+ id: 'form-draft',
+ storageKey: 'draft-data',
+ getKey: (item) => item.id,
+ })
+)
+
+const serverCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ['items'],
+ queryFn: async () => api.items.getAll(),
+ getKey: (item) => item.id,
+ onInsert: async ({ transaction }) => {
+ await api.items.create(transaction.mutations[0].modified)
+ },
+ })
+)
+
+const tx = createTransaction({
+ mutationFn: async ({ transaction }) => {
+ // Handle server collection mutations explicitly in mutationFn
+ await Promise.all(
+ transaction.mutations
+ .filter((m) => m.collection === serverCollection)
+ .map((m) => api.items.create(m.modified))
+ )
+
+ // After server mutations succeed, persist local collection mutations
+ localData.utils.acceptMutations(transaction)
+ },
+})
+
+// Apply mutations to both collections in one transaction
+tx.mutate(() => {
+ localData.insert({ id: 'draft-1', data: '...' })
+ serverCollection.insert({ id: '1', name: 'Item' })
+})
+
+await tx.commit()
+```
+
+## Complete Example
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { localStorageCollectionOptions } from '@tanstack/react-db'
+import { useLiveQuery } from '@tanstack/react-db'
+import { z } from 'zod'
+
+// Define schema
+const userPrefsSchema = z.object({
+ id: z.string(),
+ theme: z.enum(['light', 'dark', 'auto']),
+ language: z.string(),
+ notifications: z.boolean(),
+})
+
+type UserPrefs = z.infer
+
+// Create collection
+export const userPreferencesCollection = createCollection(
+ localStorageCollectionOptions({
+ id: 'user-preferences',
+ storageKey: 'app-user-prefs',
+ getKey: (item) => item.id,
+ schema: userPrefsSchema,
+ })
+)
+
+// Use in component
+function SettingsPanel() {
+ const { data: prefs } = useLiveQuery((q) =>
+ q.from({ pref: userPreferencesCollection })
+ .where(({ pref }) => pref.id === 'current-user')
+ )
+
+ const currentPrefs = prefs[0]
+
+ const updateTheme = (theme: 'light' | 'dark' | 'auto') => {
+ if (currentPrefs) {
+ userPreferencesCollection.update(currentPrefs.id, (draft) => {
+ draft.theme = theme
+ })
+ } else {
+ userPreferencesCollection.insert({
+ id: 'current-user',
+ theme,
+ language: 'en',
+ notifications: true,
+ })
+ }
+ }
+
+ return (
+
+
Theme: {currentPrefs?.theme}
+ updateTheme('dark')}>Dark Mode
+ updateTheme('light')}>Light Mode
+
+ )
+}
+```
+
+## Use Cases
+
+LocalStorage collections are perfect for:
+- User preferences and settings
+- UI state that should persist across sessions
+- Form drafts
+- Recently viewed items
+- User-specific configurations
+- Small amounts of cached data
+
+## Learn More
+
+- [Optimistic Mutations](../guides/mutations.md)
+- [Live Queries](../guides/live-queries.md)
+- [LocalOnly Collection](./local-only-collection.md)
diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md
new file mode 100644
index 000000000..afc665836
--- /dev/null
+++ b/docs/collections/powersync-collection.md
@@ -0,0 +1,475 @@
+---
+title: PowerSync Collection
+---
+
+# PowerSync Collection
+
+PowerSync collections provide seamless integration between TanStack DB and [PowerSync](https://powersync.com), enabling automatic synchronization between your in-memory TanStack DB collections and PowerSync's SQLite database. This gives you offline-ready persistence, real-time sync capabilities, and powerful conflict resolution.
+
+## Overview
+
+The `@tanstack/powersync-db-collection` package allows you to create collections that:
+
+- Automatically mirror the state of an underlying PowerSync SQLite database
+- Reactively update when PowerSync records change
+- Support optimistic mutations with rollback on error
+- Provide persistence handlers to keep PowerSync in sync with TanStack DB transactions
+- Use PowerSync's efficient SQLite-based storage engine
+- Work with PowerSync's real-time sync features for offline-first scenarios
+- Leverage PowerSync's built-in conflict resolution and data consistency guarantees
+- Enable real-time synchronization with PostgreSQL, MongoDB and MySQL backends
+
+## 1. Installation
+
+Install the PowerSync collection package along with your preferred framework integration.
+PowerSync currently works with Web, React Native and Node.js. The examples below use the Web SDK.
+See the PowerSync quickstart [docs](https://docs.powersync.com/installation/quickstart-guide) for more details.
+
+```bash
+npm install @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite
+```
+
+### 2. Create a PowerSync Database and Schema
+
+```ts
+import { Schema, Table, column } from "@powersync/web"
+
+// Define your schema
+const APP_SCHEMA = new Schema({
+ documents: new Table({
+ name: column.text,
+ author: column.text,
+ created_at: column.text,
+ archived: column.integer,
+ }),
+})
+
+// Initialize PowerSync database
+const db = new PowerSyncDatabase({
+ database: {
+ dbFilename: "app.sqlite",
+ },
+ schema: APP_SCHEMA,
+})
+```
+
+### 3. (optional) Configure Sync with a Backend
+
+```ts
+import {
+ AbstractPowerSyncDatabase,
+ PowerSyncBackendConnector,
+ PowerSyncCredentials,
+} from "@powersync/web"
+
+// TODO implement your logic here
+class Connector implements PowerSyncBackendConnector {
+ fetchCredentials: () => Promise
+
+ /** Upload local changes to the app backend.
+ *
+ * Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload.
+ *
+ * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds).
+ */
+ uploadData: (database: AbstractPowerSyncDatabase) => Promise
+}
+
+// Configure the client to connect to a PowerSync service and your backend
+db.connect(new Connector())
+```
+
+### 4. Create a TanStack DB Collection
+
+There are two main ways to create a collection: using type inference or using schema validation. Type inference will infer collection types from the underlying PowerSync SQLite tables. Schema validation can be used for additional input/output validations and type transforms.
+
+#### Option 1: Using Table Type Inference
+
+The collection types are automatically inferred from the PowerSync schema table definition. The table is used to construct a default standard schema validator which is used internally to validate collection operations.
+
+Collection mutations accept SQLite types and queries report data with SQLite types.
+
+```ts
+import { createCollection } from "@tanstack/react-db"
+import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
+
+const documentsCollection = createCollection(
+ powerSyncCollectionOptions({
+ database: db,
+ table: APP_SCHEMA.props.documents,
+ })
+)
+
+/** Note: The types for input and output are defined as this */
+// Used for mutations like `insert` or `update`
+type DocumentCollectionInput = {
+ id: string
+ name: string | null
+ author: string | null
+ created_at: string | null // SQLite TEXT
+ archived: number | null // SQLite integer
+}
+// The type of query/data results
+type DocumentCollectionOutput = DocumentCollectionInput
+```
+
+The standard PowerSync SQLite types map to these TypeScript types:
+
+| PowerSync Column Type | TypeScript Type | Description |
+| --------------------- | ---------------- | -------------------------------------------------------------------- |
+| `column.text` | `string \| null` | Text values, commonly used for strings, JSON, dates (as ISO strings) |
+| `column.integer` | `number \| null` | Integer values, also used for booleans (0/1) |
+| `column.real` | `number \| null` | Floating point numbers |
+
+Note: All PowerSync column types are nullable by default.
+
+#### Option 2: SQLite Types with Schema Validation
+
+Additional validations for collection mutations can be performed with a custom schema. The Schema below asserts that
+the `name`, `author` and `created_at` fields are required as input. `name` also has an additional string length check.
+
+Note: The input and output types specified in this example still satisfy the underlying SQLite types. An additional `deserializationSchema` is required if the typing differs. See the examples below for more details.
+
+The application logic (including the backend) should enforce that all incoming synced data passes validation with the `schema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case.
+
+```ts
+import { createCollection } from "@tanstack/react-db"
+import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
+import { z } from "zod"
+
+// Schema validates SQLite types but adds constraints
+const schema = z.object({
+ id: z.string(),
+ name: z.string().min(3, { message: "Should be at least 3 characters" }),
+ author: z.string(),
+ created_at: z.string(), // SQLite TEXT for dates
+ archived: z.number(),
+})
+
+const documentsCollection = createCollection(
+ powerSyncCollectionOptions({
+ database: db,
+ table: APP_SCHEMA.props.documents,
+ schema,
+ onDeserializationError: (error) => {
+ // Present fatal error
+ },
+ })
+)
+
+/** Note: The types for input and output are defined as this */
+// Used for mutations like `insert` or `update`
+type DocumentCollectionInput = {
+ id: string
+ name: string
+ author: string
+ created_at: string // SQLite TEXT
+ archived: number // SQLite integer
+}
+// The type of query/data results
+type DocumentCollectionOutput = DocumentCollectionInput
+```
+
+#### Option 3: Transform SQLite Input Types to Rich Output Types
+
+You can transform SQLite types to richer types (like Date objects) while keeping SQLite-compatible input types:
+
+Note: The Transformed types are provided by TanStackDB to the PowerSync SQLite persister. These types need to be serialized in
+order to be persisted to SQLite. Most types are converted by default. For custom types, override the serialization by providing a
+`serializer` param.
+
+The example below uses `nullable` columns, this is not a requirement.
+
+The application logic (including the backend) should enforce that all incoming synced data passes validation with the `schema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case.
+
+```ts
+const schema = z.object({
+ id: z.string(),
+ name: z.string().nullable(),
+ created_at: z
+ .string()
+ .nullable()
+ .transform((val) => (val ? new Date(val) : null)), // Transform SQLite TEXT to Date
+ archived: z
+ .number()
+ .nullable()
+ .transform((val) => (val != null ? val > 0 : null)), // Transform SQLite INTEGER to boolean
+})
+
+const documentsCollection = createCollection(
+ powerSyncCollectionOptions({
+ database: db,
+ table: APP_SCHEMA.props.documents,
+ schema,
+ onDeserializationError: (error) => {
+ // Present fatal error
+ },
+ // Optional: custom column serialization
+ serializer: {
+ // Dates are serialized by default, this is just an example
+ created_at: (value) => (value ? value.toISOString() : null),
+ },
+ })
+)
+
+/** Note: The types for input and output are defined as this */
+// Used for mutations like `insert` or `update`
+type DocumentCollectionInput = {
+ id: string
+ name: string | null
+ author: string | null
+ created_at: string | null // SQLite TEXT
+ archived: number | null
+}
+// The type of query/data results
+type DocumentCollectionOutput = {
+ id: string
+ name: string | null
+ author: string | null
+ created_at: Date | null // JS Date instance
+ archived: boolean | null // JS boolean
+}
+```
+
+#### Option 4: Custom Input/Output Types with Deserialization
+
+The input and output types can be completely decoupled from the internal SQLite types. This can be used to accept rich values for input mutations.
+We require an additional `deserializationSchema` in order to validate and transform incoming synced (SQLite) updates. This schema should convert the incoming SQLite update to the output type.
+
+The application logic (including the backend) should enforce that all incoming synced data passes validation with the `deserializationSchema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case.
+
+```ts
+// Our input/output types use Date and boolean
+const schema = z.object({
+ id: z.string(),
+ name: z.string(),
+ author: z.string(),
+ created_at: z.date(), // Accept Date objects as input
+ archived: z.boolean(), // Accept Booleans as input
+})
+
+// Schema to transform from SQLite types to our output types
+const deserializationSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ author: z.string(),
+ created_at: z
+ .string()
+ .transform((val) => (new Date(val))), // SQLite TEXT to Date
+ archived: z
+ .number()
+ .transform((val) => (val > 0), // SQLite INTEGER to Boolean
+})
+
+const documentsCollection = createCollection(
+ powerSyncCollectionOptions({
+ database: db,
+ table: APP_SCHEMA.props.documents,
+ schema,
+ deserializationSchema,
+ onDeserializationError: (error) => {
+ // Present fatal error
+ },
+ })
+)
+
+/** Note: The types for input and output are defined as this */
+// Used for mutations like `insert` or `update`
+type DocumentCollectionInput = {
+ id: string
+ name: string
+ author: string
+ created_at: Date
+ archived: boolean
+}
+// The type of query/data results
+type DocumentCollectionOutput = DocumentCollectionInput
+```
+
+## Features
+
+### Offline-First
+
+PowerSync collections are offline-first by default. All data is stored locally in a SQLite database, allowing your app to work without an internet connection. Changes are automatically synced when connectivity is restored.
+
+### Real-Time Sync
+
+When connected to a PowerSync backend, changes are automatically synchronized in real-time across all connected clients. The sync process handles:
+
+- Bi-directional sync with the server
+- Conflict resolution
+- Queue management for offline changes
+- Automatic retries on connection loss
+
+### Working with Rich JavaScript Types
+
+PowerSync collections support rich JavaScript types like `Date`, `Boolean`, and custom objects while maintaining SQLite compatibility. The collection handles serialization and deserialization automatically:
+
+```typescript
+import { z } from "zod"
+import { Schema, Table, column } from "@powersync/web"
+import { createCollection } from "@tanstack/react-db"
+import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
+
+// Define PowerSync SQLite schema
+const APP_SCHEMA = new Schema({
+ tasks: new Table({
+ title: column.text,
+ due_date: column.text, // Stored as ISO string in SQLite
+ completed: column.integer, // Stored as 0/1 in SQLite
+ metadata: column.text, // Stored as JSON string in SQLite
+ }),
+})
+
+// Define rich types schema
+const taskSchema = z.object({
+ id: z.string(),
+ title: z.string().nullable(),
+ due_date: z
+ .string()
+ .nullable()
+ .transform((val) => (val ? new Date(val) : null)), // Convert to Date
+ completed: z
+ .number()
+ .nullable()
+ .transform((val) => (val != null ? val > 0 : null)), // Convert to boolean
+ metadata: z
+ .string()
+ .nullable()
+ .transform((val) => (val ? JSON.parse(val) : null)), // Parse JSON
+})
+
+// Create collection with rich types
+const tasksCollection = createCollection(
+ powerSyncCollectionOptions({
+ database: db,
+ table: APP_SCHEMA.props.tasks,
+ schema: taskSchema,
+ })
+)
+
+// Work with rich types in your code
+await tasksCollection.insert({
+ id: crypto.randomUUID(),
+ title: "Review PR",
+ due_date: "2025-10-30T10:00:00Z", // String input is automatically converted to Date
+ completed: 0, // Number input is automatically converted to boolean
+ metadata: JSON.stringify({ priority: "high" }),
+})
+
+// Query returns rich types
+const task = tasksCollection.get("task-1")
+console.log(task.due_date instanceof Date) // true
+console.log(typeof task.completed) // "boolean"
+console.log(task.metadata.priority) // "high"
+```
+
+### Type Safety with Rich Types
+
+The collection maintains type safety throughout:
+
+```typescript
+type TaskInput = {
+ id: string
+ title: string | null
+ due_date: string | null // Accept ISO string for mutations
+ completed: number | null // Accept 0/1 for mutations
+ metadata: string | null // Accept JSON string for mutations
+}
+
+type TaskOutput = {
+ id: string
+ title: string | null
+ due_date: Date | null // Get Date object in queries
+ completed: boolean | null // Get boolean in queries
+ metadata: {
+ priority: string
+ [key: string]: any
+ } | null
+}
+
+// TypeScript enforces correct types:
+tasksCollection.insert({
+ due_date: new Date(), // Error: Type 'Date' is not assignable to type 'string'
+})
+
+const task = tasksCollection.get("task-1")
+task.due_date.getTime() // OK - TypeScript knows this is a Date
+```
+
+### Optimistic Updates
+
+Updates to the collection are applied optimistically to the local state first, then synchronized with PowerSync and the backend. If an error occurs during sync, the changes are automatically rolled back.
+
+## Configuration Options
+
+The `powerSyncCollectionOptions` function accepts the following options:
+
+```ts
+interface PowerSyncCollectionConfig {
+ // Required options
+ database: PowerSyncDatabase
+ table: Table
+
+ // Schema validation and type transformation
+ schema?: StandardSchemaV1
+ deserializationSchema?: StandardSchemaV1 // Required for custom input types
+ onDeserializationError?: (error: StandardSchemaV1.FailureResult) => void // Required for custom input types
+
+ // Optional Custom serialization
+ serializer?: {
+ [Key in keyof TOutput]?: (value: TOutput[Key]) => SQLiteCompatibleType
+ }
+
+ // Performance tuning
+ syncBatchSize?: number // Control batch size for initial sync, defaults to 1000
+}
+```
+
+## Advanced Transactions
+
+When you need more control over transaction handling, such as batching multiple operations or handling complex transaction scenarios, you can use PowerSync's transaction system directly with TanStack DB transactions.
+
+```ts
+import { createTransaction } from "@tanstack/react-db"
+import { PowerSyncTransactor } from "@tanstack/powersync-db-collection"
+
+// Create a transaction that won't auto-commit
+const batchTx = createTransaction({
+ autoCommit: false,
+ mutationFn: async ({ transaction }) => {
+ // Use PowerSyncTransactor to apply the transaction to PowerSync
+ await new PowerSyncTransactor({ database: db }).applyTransaction(
+ transaction
+ )
+ },
+})
+
+// Perform multiple operations in the transaction
+batchTx.mutate(() => {
+ // Add multiple documents in a single transaction
+ for (let i = 0; i < 5; i++) {
+ documentsCollection.insert({
+ id: crypto.randomUUID(),
+ name: `Document ${i}`,
+ content: `Content ${i}`,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ })
+ }
+})
+
+// Commit the transaction
+await batchTx.commit()
+
+// Wait for the changes to be persisted
+await batchTx.isPersisted.promise
+```
+
+This approach allows you to:
+
+- Batch multiple operations into a single transaction
+- Control when the transaction is committed
+- Ensure all operations are atomic
+- Wait for persistence confirmation
+- Handle complex transaction scenarios
diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md
new file mode 100644
index 000000000..4f43082d7
--- /dev/null
+++ b/docs/collections/query-collection.md
@@ -0,0 +1,706 @@
+---
+title: Query Collection
+---
+
+# Query Collection
+
+Query collections provide seamless integration between TanStack DB and TanStack Query, enabling automatic synchronization between your local database and remote data sources.
+
+## Overview
+
+The `@tanstack/query-db-collection` package allows you to create collections that:
+
+- Automatically fetch remote data via TanStack Query
+- Support optimistic updates with automatic rollback on errors
+- Handle persistence through customizable mutation handlers
+- Provide direct write capabilities for directly writing to the sync store
+
+## Installation
+
+```bash
+npm install @tanstack/query-db-collection @tanstack/query-core @tanstack/db
+```
+
+## Basic Usage
+
+```typescript
+import { QueryClient } from "@tanstack/query-core"
+import { createCollection } from "@tanstack/db"
+import { queryCollectionOptions } from "@tanstack/query-db-collection"
+
+const queryClient = new QueryClient()
+
+const todosCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ["todos"],
+ queryFn: async () => {
+ const response = await fetch("/api/todos")
+ return response.json()
+ },
+ queryClient,
+ getKey: (item) => item.id,
+ })
+)
+```
+
+## Configuration Options
+
+The `queryCollectionOptions` function accepts the following options:
+
+### Required Options
+
+- `queryKey`: The query key for TanStack Query
+- `queryFn`: Function that fetches data from the server
+- `queryClient`: TanStack Query client instance
+- `getKey`: Function to extract the unique key from an item
+
+### Query Options
+
+- `select`: Function that lets extract array items when they're wrapped with metadata
+- `enabled`: Whether the query should automatically run (default: `true`)
+- `refetchInterval`: Refetch interval in milliseconds (default: 0 β set an interval to enable polling refetching)
+- `retry`: Retry configuration for failed queries
+- `retryDelay`: Delay between retries
+- `staleTime`: How long data is considered fresh
+- `meta`: Optional metadata that will be passed to the query function context
+
+### Collection Options
+
+- `id`: Unique identifier for the collection
+- `schema`: Schema for validating items
+- `sync`: Custom sync configuration
+- `startSync`: Whether to start syncing immediately (default: `true`)
+
+### Persistence Handlers
+
+- `onInsert`: Handler called before insert operations
+- `onUpdate`: Handler called before update operations
+- `onDelete`: Handler called before delete operations
+
+## Persistence Handlers
+
+You can define handlers that are called when mutations occur. These handlers can persist changes to your backend and control whether the query should refetch after the operation:
+
+```typescript
+const todosCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ["todos"],
+ queryFn: fetchTodos,
+ queryClient,
+ getKey: (item) => item.id,
+
+ onInsert: async ({ transaction }) => {
+ const newItems = transaction.mutations.map((m) => m.modified)
+ await api.createTodos(newItems)
+ // Returning nothing or { refetch: true } will trigger a refetch
+ // Return { refetch: false } to skip automatic refetch
+ },
+
+ onUpdate: async ({ transaction }) => {
+ const updates = transaction.mutations.map((m) => ({
+ id: m.key,
+ changes: m.changes,
+ }))
+ await api.updateTodos(updates)
+ },
+
+ onDelete: async ({ transaction }) => {
+ const ids = transaction.mutations.map((m) => m.key)
+ await api.deleteTodos(ids)
+ },
+ })
+)
+```
+
+### Controlling Refetch Behavior
+
+By default, after any persistence handler (`onInsert`, `onUpdate`, or `onDelete`) completes successfully, the query will automatically refetch to ensure the local state matches the server state.
+
+You can control this behavior by returning an object with a `refetch` property:
+
+```typescript
+onInsert: async ({ transaction }) => {
+ await api.createTodos(transaction.mutations.map((m) => m.modified))
+
+ // Skip the automatic refetch
+ return { refetch: false }
+}
+```
+
+This is useful when:
+
+- You're confident the server state matches what you sent
+- You want to avoid unnecessary network requests
+- You're handling state updates through other mechanisms (like WebSockets)
+
+## Utility Methods
+
+The collection provides these utility methods via `collection.utils`:
+
+- `refetch(opts?)`: Manually trigger a refetch of the query
+ - `opts.throwOnError`: Whether to throw an error if the refetch fails (default: `false`)
+ - Bypasses `enabled: false` to support imperative/manual refetching patterns (similar to hook `refetch()` behavior)
+ - Returns `QueryObserverResult` for inspecting the result
+
+## Direct Writes
+
+Direct writes are intended for scenarios where the normal query/mutation flow doesn't fit your needs. They allow you to write directly to the synced data store, bypassing the optimistic update system and query refetch mechanism.
+
+### Understanding the Data Stores
+
+Query Collections maintain two data stores:
+
+1. **Synced Data Store** - The authoritative state synchronized with the server via `queryFn`
+2. **Optimistic Mutations Store** - Temporary changes that are applied optimistically before server confirmation
+
+Normal collection operations (insert, update, delete) create optimistic mutations that are:
+
+- Applied immediately to the UI
+- Sent to the server via persistence handlers
+- Rolled back automatically if the server request fails
+- Replaced with server data when the query refetches
+
+Direct writes bypass this system entirely and write directly to the synced data store, making them ideal for handling real-time updates from alternative sources.
+
+### When to Use Direct Writes
+
+Direct writes should be used when:
+
+- You need to sync real-time updates from WebSockets or server-sent events
+- You're dealing with large datasets where refetching everything is too expensive
+- You receive incremental updates or server-computed field updates
+- You need to implement complex pagination or partial data loading scenarios
+
+### Individual Write Operations
+
+```typescript
+// Insert a new item directly to the synced data store
+todosCollection.utils.writeInsert({
+ id: "1",
+ text: "Buy milk",
+ completed: false,
+})
+
+// Update an existing item in the synced data store
+todosCollection.utils.writeUpdate({ id: "1", completed: true })
+
+// Delete an item from the synced data store
+todosCollection.utils.writeDelete("1")
+
+// Upsert (insert or update) in the synced data store
+todosCollection.utils.writeUpsert({
+ id: "1",
+ text: "Buy milk",
+ completed: false,
+})
+```
+
+These operations:
+
+- Write directly to the synced data store
+- Do NOT create optimistic mutations
+- Do NOT trigger automatic query refetches
+- Update the TanStack Query cache immediately
+- Are immediately visible in the UI
+
+### Batch Operations
+
+The `writeBatch` method allows you to perform multiple operations atomically. Any write operations called within the callback will be collected and executed as a single transaction:
+
+```typescript
+todosCollection.utils.writeBatch(() => {
+ todosCollection.utils.writeInsert({ id: "1", text: "Buy milk" })
+ todosCollection.utils.writeInsert({ id: "2", text: "Walk dog" })
+ todosCollection.utils.writeUpdate({ id: "3", completed: true })
+ todosCollection.utils.writeDelete("4")
+})
+```
+
+### Real-World Example: WebSocket Integration
+
+```typescript
+// Handle real-time updates from WebSocket without triggering full refetches
+ws.on("todos:update", (changes) => {
+ todosCollection.utils.writeBatch(() => {
+ changes.forEach((change) => {
+ switch (change.type) {
+ case "insert":
+ todosCollection.utils.writeInsert(change.data)
+ break
+ case "update":
+ todosCollection.utils.writeUpdate(change.data)
+ break
+ case "delete":
+ todosCollection.utils.writeDelete(change.id)
+ break
+ }
+ })
+ })
+})
+```
+
+### Example: Incremental Updates
+
+When the server returns computed fields (like server-generated IDs or timestamps), you can use the `onInsert` handler with `{ refetch: false }` to avoid unnecessary refetches while still syncing the server response:
+
+```typescript
+const todosCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ["todos"],
+ queryFn: fetchTodos,
+ queryClient,
+ getKey: (item) => item.id,
+
+ onInsert: async ({ transaction }) => {
+ const newItems = transaction.mutations.map((m) => m.modified)
+
+ // Send to server and get back items with server-computed fields
+ const serverItems = await api.createTodos(newItems)
+
+ // Sync server-computed fields (like server-generated IDs, timestamps, etc.)
+ // to the collection's synced data store
+ todosCollection.utils.writeBatch(() => {
+ serverItems.forEach((serverItem) => {
+ todosCollection.utils.writeInsert(serverItem)
+ })
+ })
+
+ // Skip automatic refetch since we've already synced the server response
+ // (optimistic state is automatically replaced when handler completes)
+ return { refetch: false }
+ },
+
+ onUpdate: async ({ transaction }) => {
+ const updates = transaction.mutations.map((m) => ({
+ id: m.key,
+ changes: m.changes,
+ }))
+ const serverItems = await api.updateTodos(updates)
+
+ // Sync server-computed fields from the update response
+ todosCollection.utils.writeBatch(() => {
+ serverItems.forEach((serverItem) => {
+ todosCollection.utils.writeUpdate(serverItem)
+ })
+ })
+
+ return { refetch: false }
+ },
+ })
+)
+
+// Usage is just like a regular collection
+todosCollection.insert({ text: "Buy milk", completed: false })
+```
+
+### Example: Large Dataset Pagination
+
+```typescript
+// Load additional pages without refetching existing data
+const loadMoreTodos = async (page) => {
+ const newTodos = await api.getTodos({ page, limit: 50 })
+
+ // Add new items without affecting existing ones
+ todosCollection.utils.writeBatch(() => {
+ newTodos.forEach((todo) => {
+ todosCollection.utils.writeInsert(todo)
+ })
+ })
+}
+```
+
+## Important Behaviors
+
+### Full State Sync
+
+The query collection treats the `queryFn` result as the **complete state** of the collection. This means:
+
+- Items present in the collection but not in the query result will be deleted
+- Items in the query result but not in the collection will be inserted
+- Items present in both will be updated if they differ
+
+### Empty Array Behavior
+
+When `queryFn` returns an empty array, **all items in the collection will be deleted**. This is because the collection interprets an empty array as "the server has no items".
+
+```typescript
+// This will delete all items in the collection
+queryFn: async () => []
+```
+
+### Handling Partial/Incremental Fetches
+
+Since the query collection expects `queryFn` to return the complete state, you can handle partial fetches by merging new data with existing data:
+
+```typescript
+const todosCollection = createCollection(
+ queryCollectionOptions({
+ queryKey: ["todos"],
+ queryFn: async ({ queryKey }) => {
+ // Get existing data from cache
+ const existingData = queryClient.getQueryData(queryKey) || []
+
+ // Fetch only new/updated items (e.g., changes since last sync)
+ const lastSyncTime = localStorage.getItem("todos-last-sync")
+ const newData = await fetch(`/api/todos?since=${lastSyncTime}`).then(
+ (r) => r.json()
+ )
+
+ // Merge new data with existing data
+ const existingMap = new Map(existingData.map((item) => [item.id, item]))
+
+ // Apply updates and additions
+ newData.forEach((item) => {
+ existingMap.set(item.id, item)
+ })
+
+ // Handle deletions if your API provides them
+ if (newData.deletions) {
+ newData.deletions.forEach((id) => existingMap.delete(id))
+ }
+
+ // Update sync time
+ localStorage.setItem("todos-last-sync", new Date().toISOString())
+
+ // Return the complete merged state
+ return Array.from(existingMap.values())
+ },
+ queryClient,
+ getKey: (item) => item.id,
+ })
+)
+```
+
+This pattern allows you to:
+
+- Fetch only incremental changes from your API
+- Merge those changes with existing data
+- Return the complete state that the collection expects
+- Avoid the performance overhead of fetching all data every time
+
+### Direct Writes and Query Sync
+
+Direct writes update the collection immediately and also update the TanStack Query cache. However, they do not prevent the normal query sync behavior. If your `queryFn` returns data that conflicts with your direct writes, the query data will take precedence.
+
+To handle this properly:
+
+1. Use `{ refetch: false }` in your persistence handlers when using direct writes
+2. Set appropriate `staleTime` to prevent unnecessary refetches
+3. Design your `queryFn` to be aware of incremental updates (e.g., only fetch new data)
+
+## Complete Direct Write API Reference
+
+All direct write methods are available on `collection.utils`:
+
+- `writeInsert(data)`: Insert one or more items directly
+- `writeUpdate(data)`: Update one or more items directly
+- `writeDelete(keys)`: Delete one or more items directly
+- `writeUpsert(data)`: Insert or update one or more items directly
+- `writeBatch(callback)`: Perform multiple operations atomically
+- `refetch(opts?)`: Manually trigger a refetch of the query
+
+## QueryFn and Predicate Push-Down
+
+When using `syncMode: 'on-demand'`, the collection automatically pushes down query predicates (where clauses, orderBy, and limit) to your `queryFn`. This allows you to fetch only the data needed for each specific query, rather than fetching the entire dataset.
+
+### How LoadSubsetOptions Are Passed
+
+LoadSubsetOptions are passed to your `queryFn` via the query context's `meta` property:
+
+```typescript
+queryFn: async (ctx) => {
+ // Extract LoadSubsetOptions from the context
+ const { limit, where, orderBy } = ctx.meta.loadSubsetOptions
+
+ // Use these to fetch only the data you need
+ // ...
+}
+```
+
+The `where` and `orderBy` fields are expression trees (AST - Abstract Syntax Tree) that need to be parsed. TanStack DB provides helper functions to make this easy.
+
+### Expression Helpers
+
+```typescript
+import {
+ parseWhereExpression,
+ parseOrderByExpression,
+ extractSimpleComparisons,
+ parseLoadSubsetOptions,
+} from '@tanstack/db'
+// Or from '@tanstack/query-db-collection' (re-exported for convenience)
+```
+
+These helpers allow you to parse expression trees without manually traversing complex AST structures.
+
+### Quick Start: Simple REST API
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { queryCollectionOptions } from '@tanstack/query-db-collection'
+import { parseLoadSubsetOptions } from '@tanstack/db'
+import { QueryClient } from '@tanstack/query-core'
+
+const queryClient = new QueryClient()
+
+const productsCollection = createCollection(
+ queryCollectionOptions({
+ id: 'products',
+ queryKey: ['products'],
+ queryClient,
+ getKey: (item) => item.id,
+ syncMode: 'on-demand', // Enable predicate push-down
+
+ queryFn: async (ctx) => {
+ const { limit, where, orderBy } = ctx.meta.loadSubsetOptions
+
+ // Parse the expressions into simple format
+ const parsed = parseLoadSubsetOptions({ where, orderBy, limit })
+
+ // Build query parameters from parsed filters
+ const params = new URLSearchParams()
+
+ // Add filters
+ parsed.filters.forEach(({ field, operator, value }) => {
+ const fieldName = field.join('.')
+ if (operator === 'eq') {
+ params.set(fieldName, String(value))
+ } else if (operator === 'lt') {
+ params.set(`${fieldName}_lt`, String(value))
+ } else if (operator === 'gt') {
+ params.set(`${fieldName}_gt`, String(value))
+ }
+ })
+
+ // Add sorting
+ if (parsed.sorts.length > 0) {
+ const sortParam = parsed.sorts
+ .map(s => `${s.field.join('.')}:${s.direction}`)
+ .join(',')
+ params.set('sort', sortParam)
+ }
+
+ // Add limit
+ if (parsed.limit) {
+ params.set('limit', String(parsed.limit))
+ }
+
+ const response = await fetch(`/api/products?${params}`)
+ return response.json()
+ },
+ })
+)
+
+// Usage with live queries
+import { createLiveQueryCollection } from '@tanstack/react-db'
+import { eq, lt, and } from '@tanstack/db'
+
+const affordableElectronics = createLiveQueryCollection({
+ query: (q) =>
+ q.from({ product: productsCollection })
+ .where(({ product }) => and(
+ eq(product.category, 'electronics'),
+ lt(product.price, 100)
+ ))
+ .orderBy(({ product }) => product.price, 'asc')
+ .limit(10)
+ .select(({ product }) => product)
+})
+
+// This triggers a queryFn call with:
+// GET /api/products?category=electronics&price_lt=100&sort=price:asc&limit=10
+```
+
+### Custom Handlers for Complex APIs
+
+For APIs with specific formats, use custom handlers:
+
+```typescript
+queryFn: async (ctx) => {
+ const { where, orderBy, limit } = ctx.meta.loadSubsetOptions
+
+ // Use custom handlers to match your API's format
+ const filters = parseWhereExpression(where, {
+ handlers: {
+ eq: (field, value) => ({
+ field: field.join('.'),
+ op: 'equals',
+ value
+ }),
+ lt: (field, value) => ({
+ field: field.join('.'),
+ op: 'lessThan',
+ value
+ }),
+ and: (...conditions) => ({
+ operator: 'AND',
+ conditions
+ }),
+ or: (...conditions) => ({
+ operator: 'OR',
+ conditions
+ }),
+ }
+ })
+
+ const sorts = parseOrderByExpression(orderBy)
+
+ return api.query({
+ filters,
+ sort: sorts.map(s => ({
+ field: s.field.join('.'),
+ order: s.direction.toUpperCase()
+ })),
+ limit
+ })
+}
+```
+
+### GraphQL Example
+
+```typescript
+queryFn: async (ctx) => {
+ const { where, orderBy, limit } = ctx.meta.loadSubsetOptions
+
+ // Convert to a GraphQL where clause format
+ const whereClause = parseWhereExpression(where, {
+ handlers: {
+ eq: (field, value) => ({
+ [field.join('_')]: { _eq: value }
+ }),
+ lt: (field, value) => ({
+ [field.join('_')]: { _lt: value }
+ }),
+ and: (...conditions) => ({ _and: conditions }),
+ or: (...conditions) => ({ _or: conditions }),
+ }
+ })
+
+ // Convert to a GraphQL order_by format
+ const sorts = parseOrderByExpression(orderBy)
+ const orderByClause = sorts.map(s => ({
+ [s.field.join('_')]: s.direction
+ }))
+
+ const { data } = await graphqlClient.query({
+ query: gql`
+ query GetProducts($where: product_bool_exp, $orderBy: [product_order_by!], $limit: Int) {
+ product(where: $where, order_by: $orderBy, limit: $limit) {
+ id
+ name
+ category
+ price
+ }
+ }
+ `,
+ variables: {
+ where: whereClause,
+ orderBy: orderByClause,
+ limit
+ }
+ })
+
+ return data.product
+}
+```
+
+### Expression Helper API Reference
+
+#### `parseLoadSubsetOptions(options)`
+
+Convenience function that parses all LoadSubsetOptions at once. Good for simple use cases.
+
+```typescript
+const { filters, sorts, limit } = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
+// filters: [{ field: ['category'], operator: 'eq', value: 'electronics' }]
+// sorts: [{ field: ['price'], direction: 'asc', nulls: 'last' }]
+// limit: 10
+```
+
+#### `parseWhereExpression(expr, options)`
+
+Parses a WHERE expression using custom handlers for each operator. Use this for complete control over the output format.
+
+```typescript
+const filters = parseWhereExpression(where, {
+ handlers: {
+ eq: (field, value) => ({ [field.join('.')]: value }),
+ lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),
+ and: (...filters) => Object.assign({}, ...filters)
+ },
+ onUnknownOperator: (operator, args) => {
+ console.warn(`Unsupported operator: ${operator}`)
+ return null
+ }
+})
+```
+
+#### `parseOrderByExpression(orderBy)`
+
+Parses an ORDER BY expression into a simple array.
+
+```typescript
+const sorts = parseOrderByExpression(orderBy)
+// Returns: [{ field: ['price'], direction: 'asc', nulls: 'last' }]
+```
+
+#### `extractSimpleComparisons(expr)`
+
+Extracts simple AND-ed comparisons from a WHERE expression. Note: Only works for simple AND conditions.
+
+```typescript
+const comparisons = extractSimpleComparisons(where)
+// Returns: [
+// { field: ['category'], operator: 'eq', value: 'electronics' },
+// { field: ['price'], operator: 'lt', value: 100 }
+// ]
+```
+
+### Supported Operators
+
+- `eq` - Equality (=)
+- `gt` - Greater than (>)
+- `gte` - Greater than or equal (>=)
+- `lt` - Less than (<)
+- `lte` - Less than or equal (<=)
+- `and` - Logical AND
+- `or` - Logical OR
+- `in` - IN clause
+
+### Using Query Key Builders
+
+Create different cache entries for different filter combinations:
+
+```typescript
+const productsCollection = createCollection(
+ queryCollectionOptions({
+ id: 'products',
+ // Dynamic query key based on filters
+ queryKey: (opts) => {
+ const parsed = parseLoadSubsetOptions(opts)
+ const cacheKey = ['products']
+
+ parsed.filters.forEach(f => {
+ cacheKey.push(`${f.field.join('.')}-${f.operator}-${f.value}`)
+ })
+
+ if (parsed.limit) {
+ cacheKey.push(`limit-${parsed.limit}`)
+ }
+
+ return cacheKey
+ },
+ queryClient,
+ getKey: (item) => item.id,
+ syncMode: 'on-demand',
+ queryFn: async (ctx) => { /* ... */ },
+ })
+)
+```
+
+### Tips
+
+1. **Start with `parseLoadSubsetOptions`** for simple use cases
+2. **Use custom handlers** via `parseWhereExpression` for APIs with specific formats
+3. **Handle unsupported operators** with the `onUnknownOperator` callback
+4. **Log parsed results** during development to verify correctness
diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md
new file mode 100644
index 000000000..71ea3ffed
--- /dev/null
+++ b/docs/collections/rxdb-collection.md
@@ -0,0 +1,134 @@
+---
+title: RxDB Collection
+---
+
+# RxDB Collection
+
+RxDB collections provide seamless integration between TanStack DB and [RxDB](https://rxdb.info), enabling automatic synchronization between your in-memory TanStack DB collections and RxDB's local-first database. Giving you offline-ready persistence, and powerful sync capabilities with a wide range of backends.
+
+
+## Overview
+
+The `@tanstack/rxdb-db-collection` package allows you to create collections that:
+- Automatically mirror the state of an underlying RxDB collection
+- Reactively update when RxDB documents change
+- Support optimistic mutations with rollback on error
+- Provide persistence handlers to keep RxDB in sync with TanStack DB transactions
+- Sync across browser tabs - changes in one tab are reflected in RxDB and TanStack DB collections in all tabs
+- Use one of RxDB's [storage engines](https://rxdb.info/rx-storage.html).
+- Work with RxDB's [replication features](https://rxdb.info/replication.html) for offline-first and sync scenarios
+- Leverage RxDB's [replication plugins](https://rxdb.info/replication.html) to sync with CouchDB, MongoDB, Supabase, REST APIs, GraphQL, WebRTC (P2P) and more.
+
+
+## 1. Installation
+
+Install the RXDB collection packages along with your preferred framework integration.
+
+```bash
+npm install @tanstack/rxdb-db-collection rxdb @tanstack/react-db
+```
+
+
+### 2. Create an RxDatabase and RxCollection
+
+```ts
+import { createRxDatabase, addRxPlugin } from 'rxdb/plugins/core'
+
+/**
+ * Here we use the localstorage based storage for RxDB.
+ * RxDB has a wide range of storages based on Dexie.js, IndexedDB, SQLite and more.
+ */
+import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage'
+
+// add json-schema validation (optional)
+import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
+
+// Enable dev mode (optional, recommended during development)
+import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode'
+addRxPlugin(RxDBDevModePlugin)
+
+type Todo = { id: string; text: string; completed: boolean }
+
+const db = await createRxDatabase({
+ name: 'my-todos',
+ storage: wrappedValidateAjvStorage({
+ storage: getRxStorageLocalstorage()
+ })
+})
+
+await db.addCollections({
+ todos: {
+ schema: {
+ title: 'todos',
+ version: 0,
+ type: 'object',
+ primaryKey: 'id',
+ properties: {
+ id: { type: 'string', maxLength: 100 },
+ text: { type: 'string' },
+ completed: { type: 'boolean' },
+ },
+ required: ['id', 'text', 'completed'],
+ },
+ },
+})
+```
+
+
+### 3. (optional) sync with a backend
+```ts
+import { replicateRxCollection } from 'rxdb/plugins/replication'
+const replicationState = replicateRxCollection({
+ collection: db.todos,
+ pull: { handler: myPullHandler },
+ push: { handler: myPushHandler },
+})
+```
+
+### 4. Wrap the RxDB collection with TanStack DB
+
+```ts
+import { createCollection } from '@tanstack/react-db'
+import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection'
+
+const todosCollection = createCollection(
+ rxdbCollectionOptions({
+ rxCollection: myDatabase.todos,
+ startSync: true, // start ingesting RxDB data immediately
+ })
+)
+```
+
+
+Now `todosCollection` is a reactive TanStack DB collection driven by RxDB:
+
+- Writes via `todosCollection.insert/update/delete` persist to RxDB.
+- Direct writes in RxDB (or via replication) flow into the TanStack collection via change streams.
+
+
+
+## Configuration Options
+
+The `rxdbCollectionOptions` function accepts the following options:
+
+### Required
+
+- `rxCollection`: The underlying [RxDB collection](https://rxdb.info/rx-collection.html)
+
+### Optional
+
+- `id`: Unique identifier for the collection
+- `schema`: Schema for validating items. RxDB already has schema validation but having additional validation on the TanStack DB side can help to unify error handling between different tanstack collections.
+- `startSync`: Whether to start syncing immediately (default: true)
+- `onInsert, onUpdate, onDelete`: Override default persistence handlers. By default, TanStack DB writes are persisted to RxDB using bulkUpsert, patch, and bulkRemove.
+- `syncBatchSize`: The maximum number of documents fetched per batch during the initial sync from RxDB into TanStack DB (default: 1000). Larger values reduce round trips but use more memory; smaller values are lighter but may increase query calls. Note that this only affects the initial sync. Ongoing live updates are streamed one by one via RxDB's change feed.
+
+
+
+## Syncing with Backends
+
+Replication and sync in RxDB run independently of TanStack DB. You set up replication directly on your RxCollection using RxDB's replication plugins (for CouchDB, GraphQL, WebRTC, REST APIs, etc.).
+
+When replication runs, it pulls and pushes changes to the backend and applies them to the RxDB collection. Since the TanStack DB integration subscribes to the RxDB change stream, any changes applied by replication are automatically reflected in your TanStack DB collection.
+
+This separation of concerns means you configure replication entirely in RxDB, and TanStack DB automatically benefits: your TanStack collections always stay up to date with whatever sync strategy you choose.
diff --git a/docs/collections/trailbase-collection.md b/docs/collections/trailbase-collection.md
new file mode 100644
index 000000000..1b1d60d42
--- /dev/null
+++ b/docs/collections/trailbase-collection.md
@@ -0,0 +1,226 @@
+---
+title: TrailBase Collection
+---
+
+# TrailBase Collection
+
+TrailBase collections provide seamless integration between TanStack DB and [TrailBase](https://trailbase.io), enabling real-time data synchronization with TrailBase's self-hosted application backend.
+
+## Overview
+
+[TrailBase](https://trailbase.io) is an easy-to-self-host, single-executable application backend with built-in SQLite, a V8 JS runtime, auth, admin UIs and sync functionality.
+
+The `@tanstack/trailbase-db-collection` package allows you to create collections that:
+- Automatically sync data from TrailBase Record APIs
+- Support real-time subscriptions when `enable_subscriptions` is enabled
+- Handle optimistic updates with automatic rollback on errors
+- Provide parse/serialize functions for data transformation
+
+## Installation
+
+```bash
+npm install @tanstack/trailbase-db-collection @tanstack/react-db trailbase
+```
+
+## Basic Usage
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection'
+import { initClient } from 'trailbase'
+
+const trailBaseClient = initClient(`https://your-trailbase-instance.com`)
+
+const todosCollection = createCollection(
+ trailBaseCollectionOptions({
+ id: 'todos',
+ recordApi: trailBaseClient.records('todos'),
+ getKey: (item) => item.id,
+ })
+)
+```
+
+## Configuration Options
+
+The `trailBaseCollectionOptions` function accepts the following options:
+
+### Required Options
+
+- `id`: Unique identifier for the collection
+- `recordApi`: TrailBase Record API instance created via `trailBaseClient.records()`
+- `getKey`: Function to extract the unique key from an item
+
+### Optional Options
+
+- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation
+- `parse`: Object mapping field names to parsing functions that transform data coming from TrailBase
+- `serialize`: Object mapping field names to serialization functions that transform data going to TrailBase
+- `onInsert`: Handler function called when items are inserted
+- `onUpdate`: Handler function called when items are updated
+- `onDelete`: Handler function called when items are deleted
+
+## Data Transformation
+
+TrailBase uses different data formats for storage (e.g., Unix timestamps). Use `parse` and `serialize` to handle these transformations:
+
+```typescript
+type SelectTodo = {
+ id: string
+ text: string
+ created_at: number // Unix timestamp from TrailBase
+ completed: boolean
+}
+
+type Todo = {
+ id: string
+ text: string
+ created_at: Date // JavaScript Date for app usage
+ completed: boolean
+}
+
+const todosCollection = createCollection(
+ trailBaseCollectionOptions({
+ id: 'todos',
+ recordApi: trailBaseClient.records('todos'),
+ getKey: (item) => item.id,
+ schema: todoSchema,
+ // Transform TrailBase data to application format
+ parse: {
+ created_at: (ts) => new Date(ts * 1000),
+ },
+ // Transform application data to TrailBase format
+ serialize: {
+ created_at: (date) => Math.floor(date.valueOf() / 1000),
+ },
+ })
+)
+```
+
+## Real-time Subscriptions
+
+TrailBase supports real-time subscriptions when enabled on the server. The collection automatically subscribes to changes and updates in real-time:
+
+```typescript
+const todosCollection = createCollection(
+ trailBaseCollectionOptions({
+ id: 'todos',
+ recordApi: trailBaseClient.records('todos'),
+ getKey: (item) => item.id,
+ // Real-time updates work automatically when
+ // enable_subscriptions is set in TrailBase config
+ })
+)
+
+// Changes from other clients will automatically update
+// the collection in real-time
+```
+
+## Mutation Handlers
+
+Handle inserts, updates, and deletes by providing mutation handlers:
+
+```typescript
+const todosCollection = createCollection(
+ trailBaseCollectionOptions({
+ id: 'todos',
+ recordApi: trailBaseClient.records('todos'),
+ getKey: (item) => item.id,
+ onInsert: async ({ transaction }) => {
+ const newTodo = transaction.mutations[0].modified
+ // TrailBase handles the persistence automatically
+ // Add custom logic here if needed
+ },
+ onUpdate: async ({ transaction }) => {
+ const { original, modified } = transaction.mutations[0]
+ // TrailBase handles the persistence automatically
+ // Add custom logic here if needed
+ },
+ onDelete: async ({ transaction }) => {
+ const deletedTodo = transaction.mutations[0].original
+ // TrailBase handles the persistence automatically
+ // Add custom logic here if needed
+ },
+ })
+)
+```
+
+## Complete Example
+
+```typescript
+import { createCollection } from '@tanstack/react-db'
+import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection'
+import { initClient } from 'trailbase'
+import { z } from 'zod'
+
+const trailBaseClient = initClient(`https://your-trailbase-instance.com`)
+
+// Define schema
+const todoSchema = z.object({
+ id: z.string(),
+ text: z.string(),
+ completed: z.boolean(),
+ created_at: z.date(),
+})
+
+type SelectTodo = {
+ id: string
+ text: string
+ completed: boolean
+ created_at: number
+}
+
+type Todo = z.infer
+
+// Create collection
+export const todosCollection = createCollection(
+ trailBaseCollectionOptions({
+ id: 'todos',
+ recordApi: trailBaseClient.records('todos'),
+ getKey: (item) => item.id,
+ schema: todoSchema,
+ parse: {
+ created_at: (ts) => new Date(ts * 1000),
+ },
+ serialize: {
+ created_at: (date) => Math.floor(date.valueOf() / 1000),
+ },
+ onInsert: async ({ transaction }) => {
+ const newTodo = transaction.mutations[0].modified
+ console.log('Todo created:', newTodo)
+ },
+ })
+)
+
+// Use in component
+function TodoList() {
+ const { data: todos } = useLiveQuery((q) =>
+ q.from({ todo: todosCollection })
+ .where(({ todo }) => !todo.completed)
+ .orderBy(({ todo }) => todo.created_at, 'desc')
+ )
+
+ const addTodo = (text: string) => {
+ todosCollection.insert({
+ id: crypto.randomUUID(),
+ text,
+ completed: false,
+ created_at: new Date(),
+ })
+ }
+
+ return (
+
+ {todos.map((todo) => (
+
{todo.text}
+ ))}
+
+ )
+}
+```
+
+## Learn More
+
+- [TrailBase Documentation](https://trailbase.io/documentation/)
+- [TrailBase Record APIs](https://trailbase.io/documentation/apis_record/)
+- [Optimistic Mutations](../guides/mutations.md)
+- [Live Queries](../guides/live-queries.md)
diff --git a/docs/community/resources.md b/docs/community/resources.md
new file mode 100644
index 000000000..781c11963
--- /dev/null
+++ b/docs/community/resources.md
@@ -0,0 +1,52 @@
+---
+title: Community Resources
+id: community-resources
+---
+
+# Community Resources
+
+This page contains a curated list of community-created packages, tools, and resources that extend or complement TanStack DB.
+
+## Community Packages
+
+### Dexie.js Integration (Unofficial)
+- **[tanstack-dexie-db-collection](https://github.com/HimanshuKumarDutt094/tanstack-dexie-db-collection)** - Community-maintained Dexie.js adapter for TanStack DB
+ - Local persistence using [Dexie.js](https://dexie.org) (IndexedDB wrapper)
+ - Lightweight integration for browser-based storage
+ - Install: `npm install tanstack-dexie-db-collection`
+
+### PGLite Integration (Unofficial)
+- **[tanstack-db-pglite](https://github.com/letstri/tanstack-db-pglite)** - Community-maintained [PGLite](https://pglite.dev/) adapter for TanStack DB
+ - Use PostgreSQL-compatible databases in the browser via WebAssembly
+ - Install: `npm install tanstack-db-pglite`
+
+### Contributing Your Package
+
+Have you created a collection adapter or integration? We'd love to feature it here! [Submit a PR](https://github.com/TanStack/db/pulls) to add your package.
+
+## Examples & Templates
+
+### Starter Templates
+*Share your starter templates and boilerplates here*
+
+### Example Applications
+*Community-built example apps showcasing TanStack DB features*
+
+## Learning Resources
+
+### Tutorials & Guides
+*Community tutorials and blog posts about TanStack DB*
+
+### Videos & Courses
+*Video tutorials and online courses*
+
+## Contributing
+
+We welcome contributions to this community resources page! If you've created something useful for the TanStack DB ecosystem:
+
+1. Fork the repository
+2. Add your resource to the appropriate section
+3. Include a brief description and link
+4. Submit a pull request
+
+Please ensure all submissions are relevant to TanStack DB and provide value to the community.
diff --git a/docs/config.json b/docs/config.json
index 515d88d53..09ca7c62b 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -20,43 +20,98 @@
{
"label": "Installation",
"to": "installation"
- },
+ }
+ ]
+ },
+ {
+ "label": "Guides",
+ "children": [
{
"label": "Live Queries",
- "to": "live-queries"
+ "to": "guides/live-queries"
+ },
+ {
+ "label": "Mutations",
+ "to": "guides/mutations"
+ },
+ {
+ "label": "Schemas",
+ "to": "guides/schemas"
+ },
+ {
+ "label": "Error Handling",
+ "to": "guides/error-handling"
+ },
+ {
+ "label": "Creating Collection Options Creators",
+ "to": "guides/collection-options-creator"
}
- ],
- "frameworks": [
+ ]
+ },
+ {
+ "label": "Collections",
+ "children": [
{
- "label": "react",
- "children": [
- {
- "label": "React Adapter",
- "to": "framework/react/adapter"
- }
- ]
+ "label": "Query Collection",
+ "to": "collections/query-collection"
},
{
- "label": "vue",
- "children": [
- {
- "label": "Vue Adapter",
- "to": "framework/vue/adapter"
- }
- ]
+ "label": "Electric Collection",
+ "to": "collections/electric-collection"
+ },
+ {
+ "label": "TrailBase Collection",
+ "to": "collections/trailbase-collection"
+ },
+ {
+ "label": "RxDB Collection",
+ "to": "collections/rxdb-collection"
+ },
+ {
+ "label": "PowerSync Collection",
+ "to": "collections/powersync-collection"
+ },
+ {
+ "label": "LocalStorage Collection",
+ "to": "collections/local-storage-collection"
+ },
+ {
+ "label": "LocalOnly Collection",
+ "to": "collections/local-only-collection"
}
]
},
{
- "label": "Guides",
+ "label": "Frameworks",
"children": [
{
- "label": "Error Handling",
- "to": "error-handling"
+ "label": "React",
+ "to": "framework/react/overview"
},
{
- "label": "Creating Collection Options Creators",
- "to": "collection-options-creator"
+ "label": "Vue",
+ "to": "framework/vue/overview"
+ },
+ {
+ "label": "Angular",
+ "to": "framework/angular/overview"
+ },
+ {
+ "label": "Solid",
+ "to": "framework/solid/overview"
+ },
+ {
+ "label": "Svelte",
+ "to": "framework/svelte/overview"
+ }
+ ]
+ },
+ {
+ "label": "Community",
+ "children": [
+ {
+ "label": "Resources & Packages",
+ "to": "community/resources"
}
]
},
@@ -66,6 +121,62 @@
{
"label": "Core API Reference",
"to": "reference/index"
+ },
+ {
+ "label": "Collection",
+ "to": "reference/interfaces/Collection"
+ },
+ {
+ "label": "createCollection",
+ "to": "reference/functions/createCollection"
+ },
+ {
+ "label": "liveQueryCollectionOptions",
+ "to": "reference/functions/liveQueryCollectionOptions"
+ },
+ {
+ "label": "createLiveQueryCollection",
+ "to": "reference/functions/createLiveQueryCollection"
+ },
+ {
+ "label": "createOptimisticAction",
+ "to": "reference/functions/createOptimisticAction"
+ },
+ {
+ "label": "createTransaction",
+ "to": "reference/functions/createTransaction"
+ },
+ {
+ "label": "Electric DB Collection",
+ "to": "reference/electric-db-collection/index"
+ },
+ {
+ "label": "electricCollectionOptions",
+ "to": "reference/electric-db-collection/functions/electricCollectionOptions"
+ },
+ {
+ "label": "Query DB Collection",
+ "to": "reference/query-db-collection/index"
+ },
+ {
+ "label": "queryCollectionOptions",
+ "to": "reference/query-db-collection/functions/queryCollectionOptions"
+ },
+ {
+ "label": "RxDB DB Collection",
+ "to": "reference/rxdb-db-collection/index"
+ },
+ {
+ "label": "rxdbCollectionOptions",
+ "to": "reference/rxdb-db-collection/functions/rxdbCollectionOptions"
+ },
+ {
+ "label": "PowerSync Collection",
+ "to": "reference/powersync-db-collection/index"
+ },
+ {
+ "label": "powerSyncCollectionOptions",
+ "to": "reference/powersync-db-collection/functions/powerSyncCollectionOptions"
}
],
"frameworks": [
@@ -75,6 +186,23 @@
{
"label": "React Hooks",
"to": "framework/react/reference/index"
+ },
+ {
+ "label": "useLiveQuery",
+ "to": "framework/react/reference/functions/useLiveQuery"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "Solid Hooks",
+ "to": "framework/solid/reference/index"
+ },
+ {
+ "label": "useLiveQuery",
+ "to": "framework/solid/reference/functions/useLiveQuery"
}
]
},
@@ -82,23 +210,33 @@
"label": "vue",
"children": [
{
- "label": "Vue Hooks",
+ "label": "Vue Composables",
"to": "framework/vue/reference/index"
+ },
+ {
+ "label": "useLiveQuery",
+ "to": "framework/vue/reference/functions/useLiveQuery"
+ },
+ {
+ "label": "UseLiveQueryReturn",
+ "to": "framework/vue/reference/interfaces/UseLiveQueryReturn"
+ },
+ {
+ "label": "UseLiveQueryReturnWithCollection",
+ "to": "framework/vue/reference/interfaces/UseLiveQueryReturnWithCollection"
}
]
- }
- ]
- },
- {
- "label": "Examples",
- "children": [],
- "frameworks": [
+ },
{
- "label": "react",
+ "label": "angular",
"children": [
{
- "label": "Todo List",
- "to": "framework/react/examples/todo"
+ "label": "Angular Functions",
+ "to": "framework/angular/reference/index"
+ },
+ {
+ "label": "injectLiveQuery",
+ "to": "framework/angular/reference/functions/injectLiveQuery"
}
]
}
diff --git a/docs/framework/angular/overview.md b/docs/framework/angular/overview.md
new file mode 100644
index 000000000..15adcaf6c
--- /dev/null
+++ b/docs/framework/angular/overview.md
@@ -0,0 +1,212 @@
+---
+title: TanStack DB Angular Adapter
+id: adapter
+---
+
+## Installation
+
+```sh
+npm install @tanstack/angular-db
+```
+
+## Angular inject function
+
+See the [Angular Functions Reference](../reference/index.md) to see the full list of functions available in the Angular Adapter.
+
+For comprehensive documentation on writing queries (filtering, joins, aggregations, etc.), see the [Live Queries Guide](../../guides/live-queries).
+
+## Basic Usage
+
+### injectLiveQuery
+
+The `injectLiveQuery` function creates a live query that automatically updates your component when data changes. It returns an object containing Angular signals for reactive state management:
+
+```typescript
+import { Component } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+import { eq } from '@tanstack/db'
+
+@Component({
+ selector: 'app-todo-list',
+ standalone: true,
+ template: `
+ @if (query.isLoading()) {
+ Loading...
+ } @else {
+
+ @for (todo of query.data(); track todo.id) {
+ {{ todo.text }}
+ }
+
+ }
+ `
+})
+export class TodoListComponent {
+ query = injectLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+}
+```
+
+**Note:** All return values (`data`, `isLoading`, `status`, etc.) are Angular signals, so call them with `()` in your template: `query.data()`, `query.isLoading()`.
+
+> **Template Syntax:** Examples use Angular 17+ control flow (`@if`, `@for`). For Angular 16, use `*ngIf` and `*ngFor` instead.
+
+### Reactive Parameters
+
+For queries that depend on reactive values, use the `params` option to re-run the query when those values change:
+
+```typescript
+import { Component, signal } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+import { gt } from '@tanstack/db'
+
+@Component({
+ selector: 'app-filtered-todos',
+ standalone: true,
+ template: `
+ {{ query.data().length }} high-priority todos
+ `
+})
+export class FilteredTodosComponent {
+ minPriority = signal(5)
+
+ query = injectLiveQuery({
+ params: () => ({ minPriority: this.minPriority() }),
+ query: ({ params, q }) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, params.minPriority))
+ })
+}
+```
+
+#### When to Use Reactive Parameters
+
+Use the reactive `params` option when your query depends on:
+- Component signals
+- Input properties
+- Computed values
+- Other reactive state
+
+When any reactive value accessed in the `params` function changes, the query is recreated and re-executed.
+
+#### What Happens When Parameters Change
+
+When a parameter value changes:
+1. The previous live-query collection is disposed
+2. A new query is created with the updated parameter values
+3. `status()`/`isLoading()` reflect the new query's lifecycle
+4. `data()` updates automatically when the new results arrive
+
+#### Best Practices
+
+**Use reactive params for dynamic queries:**
+
+```typescript
+import { Component, Input, signal } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+import { eq, and } from '@tanstack/db'
+
+@Component({
+ selector: 'app-todo-list',
+ standalone: true,
+ template: `{{ query.data().length }} todos
`
+})
+export class TodoListComponent {
+ // Angular 16+ compatible input
+ @Input({ required: true }) userId!: number
+ status = signal('active')
+
+ // Good - reactive params track all dependencies
+ query = injectLiveQuery({
+ params: () => ({
+ userId: this.userId,
+ status: this.status()
+ }),
+ query: ({ params, q }) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => and(
+ eq(todos.userId, params.userId),
+ eq(todos.status, params.status)
+ ))
+ })
+}
+```
+
+**Using Angular 17+ signal inputs:**
+
+```typescript
+import { Component, input, signal } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+import { eq, and } from '@tanstack/db'
+
+@Component({
+ selector: 'app-todo-list',
+ standalone: true,
+ template: `{{ query.data().length }} todos
`
+})
+export class TodoListComponent {
+ // Angular 17+ signal-based input
+ userId = input.required()
+ status = signal('active')
+
+ query = injectLiveQuery({
+ params: () => ({
+ userId: this.userId(),
+ status: this.status()
+ }),
+ query: ({ params, q }) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => and(
+ eq(todos.userId, params.userId),
+ eq(todos.status, params.status)
+ ))
+ })
+}
+```
+
+**Static queries don't need params:**
+
+```typescript
+import { Component } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+
+@Component({
+ selector: 'app-all-todos',
+ standalone: true,
+ template: `{{ query.data().length }} todos
`
+})
+export class AllTodosComponent {
+ // No reactive dependencies - query never changes
+ query = injectLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ )
+}
+```
+
+**Access multiple signals in template:**
+
+```typescript
+import { Component } from '@angular/core'
+import { injectLiveQuery } from '@tanstack/angular-db'
+import { eq } from '@tanstack/db'
+
+@Component({
+ selector: 'app-todos',
+ standalone: true,
+ template: `
+ Status: {{ query.status() }}
+ Loading: {{ query.isLoading() }}
+ Ready: {{ query.isReady() }}
+ Total: {{ query.data().length }}
+ `
+})
+export class TodosComponent {
+ query = injectLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ )
+}
+```
diff --git a/docs/framework/angular/reference/functions/injectLiveQuery.md b/docs/framework/angular/reference/functions/injectLiveQuery.md
new file mode 100644
index 000000000..4b3209aca
--- /dev/null
+++ b/docs/framework/angular/reference/functions/injectLiveQuery.md
@@ -0,0 +1,120 @@
+---
+id: injectLiveQuery
+title: injectLiveQuery
+---
+
+# Function: injectLiveQuery()
+
+## Call Signature
+
+```ts
+function injectLiveQuery(options): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>;
+```
+
+Defined in: [index.ts:51](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L51)
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+#### TParams
+
+`TParams` *extends* `unknown`
+
+### Parameters
+
+#### options
+
+##### params
+
+() => `TParams`
+
+##### query
+
+(`args`) => `QueryBuilder`\<`TContext`\>
+
+### Returns
+
+[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\>
+
+## Call Signature
+
+```ts
+function injectLiveQuery(queryFn): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>;
+```
+
+Defined in: [index.ts:61](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L61)
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) => `QueryBuilder`\<`TContext`\>
+
+### Returns
+
+[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\>
+
+## Call Signature
+
+```ts
+function injectLiveQuery(config): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>;
+```
+
+Defined in: [index.ts:64](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L64)
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### config
+
+`LiveQueryCollectionConfig`\<`TContext`\>
+
+### Returns
+
+[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\>
+
+## Call Signature
+
+```ts
+function injectLiveQuery(liveQueryCollection): InjectLiveQueryResult;
+```
+
+Defined in: [index.ts:67](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L67)
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`\>
+
+### Returns
+
+[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<`TResult`, `TKey`, `TUtils`\>
diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md
new file mode 100644
index 000000000..3dfb77c21
--- /dev/null
+++ b/docs/framework/angular/reference/index.md
@@ -0,0 +1,14 @@
+---
+id: "@tanstack/angular-db"
+title: "@tanstack/angular-db"
+---
+
+# @tanstack/angular-db
+
+## Interfaces
+
+- [InjectLiveQueryResult](../interfaces/InjectLiveQueryResult.md)
+
+## Functions
+
+- [injectLiveQuery](../functions/injectLiveQuery.md)
diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md
new file mode 100644
index 000000000..aa45218bc
--- /dev/null
+++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md
@@ -0,0 +1,134 @@
+---
+id: InjectLiveQueryResult
+title: InjectLiveQueryResult
+---
+
+# Interface: InjectLiveQueryResult\
+
+Defined in: [index.ts:26](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L26)
+
+The result of calling `injectLiveQuery`.
+Contains reactive signals for the query state and data.
+
+## Type Parameters
+
+### TResult
+
+`TResult` *extends* `object` = `any`
+
+### TKey
+
+`TKey` *extends* `string` \| `number` = `string` \| `number`
+
+### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\> = \{
+\}
+
+## Properties
+
+### collection
+
+```ts
+collection: Signal, TResult>>;
+```
+
+Defined in: [index.ts:36](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L36)
+
+A signal containing the underlying collection instance
+
+***
+
+### data
+
+```ts
+data: Signal;
+```
+
+Defined in: [index.ts:34](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L34)
+
+A signal containing the results as an array
+
+***
+
+### isCleanedUp
+
+```ts
+isCleanedUp: Signal;
+```
+
+Defined in: [index.ts:48](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L48)
+
+A signal indicating whether the collection has been cleaned up
+
+***
+
+### isError
+
+```ts
+isError: Signal;
+```
+
+Defined in: [index.ts:46](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L46)
+
+A signal indicating whether the collection has an error
+
+***
+
+### isIdle
+
+```ts
+isIdle: Signal;
+```
+
+Defined in: [index.ts:44](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L44)
+
+A signal indicating whether the collection is idle
+
+***
+
+### isLoading
+
+```ts
+isLoading: Signal;
+```
+
+Defined in: [index.ts:40](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L40)
+
+A signal indicating whether the collection is currently loading
+
+***
+
+### isReady
+
+```ts
+isReady: Signal;
+```
+
+Defined in: [index.ts:42](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L42)
+
+A signal indicating whether the collection is ready
+
+***
+
+### state
+
+```ts
+state: Signal>;
+```
+
+Defined in: [index.ts:32](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L32)
+
+A signal containing the complete state map of results keyed by their ID
+
+***
+
+### status
+
+```ts
+status: Signal;
+```
+
+Defined in: [index.ts:38](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L38)
+
+A signal containing the current status of the collection
diff --git a/docs/framework/react/adapter.md b/docs/framework/react/adapter.md
deleted file mode 100644
index 7a3b17c60..000000000
--- a/docs/framework/react/adapter.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-title: TanStack DB React Adapter
-id: adapter
----
-
-## Installation
-
-```sh
-npm install @tanstack/react-db
-```
-
-## React Hooks
-
-See the [React Functions Reference](../reference/index.md) to see the full list of hooks available in the React Adapter.
-
-## Basic Usage
diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md
new file mode 100644
index 000000000..0e53916f9
--- /dev/null
+++ b/docs/framework/react/overview.md
@@ -0,0 +1,163 @@
+---
+title: TanStack DB React Adapter
+id: adapter
+---
+
+## Installation
+
+```sh
+npm install @tanstack/react-db
+```
+
+## React Hooks
+
+See the [React Functions Reference](../reference/index.md) to see the full list of hooks available in the React Adapter.
+
+For comprehensive documentation on writing queries (filtering, joins, aggregations, etc.), see the [Live Queries Guide](../../guides/live-queries).
+
+## Basic Usage
+
+### useLiveQuery
+
+The `useLiveQuery` hook creates a live query that automatically updates your component when data changes:
+
+```tsx
+import { useLiveQuery } from '@tanstack/react-db'
+
+function TodoList() {
+ const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+
+ if (isLoading) return Loading...
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+```
+
+### Dependency Arrays
+
+All query hooks (`useLiveQuery`, `useLiveInfiniteQuery`, `useLiveSuspenseQuery`) accept an optional dependency array as their last parameter. This array works similarly to React's `useEffect` dependencies - when any value in the array changes, the query is recreated and re-executed.
+
+#### When to Use Dependency Arrays
+
+Use dependency arrays when your query depends on external reactive values (props, state, or other hooks):
+
+```tsx
+function FilteredTodos({ minPriority }: { minPriority: number }) {
+ const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+ )
+
+ return {data.length} high-priority todos
+}
+```
+
+#### What Happens When Dependencies Change
+
+When a dependency value changes:
+1. The previous live query collection is cleaned up
+2. A new query is created with the updated values
+3. The component re-renders with the new data
+4. The hook suspends (for `useLiveSuspenseQuery`) or shows loading state
+
+#### Best Practices
+
+**Include all external values used in the query:**
+
+```tsx
+// Good - all external values in deps
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => and(
+ eq(todos.userId, userId),
+ eq(todos.status, status)
+ )),
+ [userId, status]
+)
+
+// Bad - missing dependencies
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.userId, userId)),
+ [] // Missing userId!
+)
+```
+
+**Empty array for static queries:**
+
+```tsx
+// No external dependencies - query never changes
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection }),
+ []
+)
+```
+
+**Omit the array for queries with no external dependencies:**
+
+```tsx
+// Same as above - no deps needed
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+)
+```
+
+### useLiveInfiniteQuery
+
+For paginated data with live updates, use `useLiveInfiniteQuery`:
+
+```tsx
+const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
+ (q) => q
+ .from({ posts: postsCollection })
+ .where(({ posts }) => eq(posts.category, category))
+ .orderBy(({ posts }) => posts.createdAt, 'desc'),
+ {
+ pageSize: 20,
+ getNextPageParam: (lastPage, allPages) =>
+ lastPage.length === 20 ? allPages.length : undefined
+ },
+ [category] // Re-run when category changes
+)
+```
+
+**Note:** The dependency array is only available when using the query function variant, not when passing a pre-created collection.
+
+### useLiveSuspenseQuery
+
+For React Suspense integration, use `useLiveSuspenseQuery`:
+
+```tsx
+function TodoList({ filter }: { filter: string }) {
+ const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.filter, filter)),
+ [filter] // Re-suspends when filter changes
+ )
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+
+function App() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+When dependencies change, `useLiveSuspenseQuery` will re-suspend, showing your Suspense fallback until the new data is ready.
diff --git a/docs/framework/react/reference/functions/useLiveInfiniteQuery.md b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md
new file mode 100644
index 000000000..49f258cf6
--- /dev/null
+++ b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md
@@ -0,0 +1,217 @@
+---
+id: useLiveInfiniteQuery
+title: useLiveInfiniteQuery
+---
+
+# Function: useLiveInfiniteQuery()
+
+## Call Signature
+
+```ts
+function useLiveInfiniteQuery(liveQueryCollection, config): UseLiveInfiniteQueryReturn;
+```
+
+Defined in: [useLiveInfiniteQuery.ts:113](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L113)
+
+Create an infinite query using a query function with live updates
+
+Uses `utils.setWindow()` to dynamically adjust the limit/offset window
+without recreating the live query collection on each page change.
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult`
+
+#### config
+
+[`UseLiveInfiniteQueryConfig`](../../type-aliases/UseLiveInfiniteQueryConfig.md)\<`any`\>
+
+Configuration including pageSize and getNextPageParam
+
+### Returns
+
+[`UseLiveInfiniteQueryReturn`](../../type-aliases/UseLiveInfiniteQueryReturn.md)\<`any`\>
+
+Object with pages, data, and pagination controls
+
+### Examples
+
+```ts
+// Basic infinite query
+const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
+ (q) => q
+ .from({ posts: postsCollection })
+ .orderBy(({ posts }) => posts.createdAt, 'desc')
+ .select(({ posts }) => ({
+ id: posts.id,
+ title: posts.title
+ })),
+ {
+ pageSize: 20,
+ getNextPageParam: (lastPage, allPages) =>
+ lastPage.length === 20 ? allPages.length : undefined
+ }
+)
+```
+
+```ts
+// With dependencies
+const { pages, fetchNextPage } = useLiveInfiniteQuery(
+ (q) => q
+ .from({ posts: postsCollection })
+ .where(({ posts }) => eq(posts.category, category))
+ .orderBy(({ posts }) => posts.createdAt, 'desc'),
+ {
+ pageSize: 10,
+ getNextPageParam: (lastPage) =>
+ lastPage.length === 10 ? lastPage.length : undefined
+ },
+ [category]
+)
+```
+
+```ts
+// Router loader pattern with pre-created collection
+// In loader:
+const postsQuery = createLiveQueryCollection({
+ query: (q) => q
+ .from({ posts: postsCollection })
+ .orderBy(({ posts }) => posts.createdAt, 'desc')
+ .limit(20)
+})
+await postsQuery.preload()
+return { postsQuery }
+
+// In component:
+const { postsQuery } = useLoaderData()
+const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
+ postsQuery,
+ {
+ pageSize: 20,
+ getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
+ }
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveInfiniteQuery(
+ queryFn,
+ config,
+deps?): UseLiveInfiniteQueryReturn;
+```
+
+Defined in: [useLiveInfiniteQuery.ts:123](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L123)
+
+Create an infinite query using a query function with live updates
+
+Uses `utils.setWindow()` to dynamically adjust the limit/offset window
+without recreating the live query collection on each page change.
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) => `QueryBuilder`\<`TContext`\>
+
+Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
+
+#### config
+
+[`UseLiveInfiniteQueryConfig`](../../type-aliases/UseLiveInfiniteQueryConfig.md)\<`TContext`\>
+
+Configuration including pageSize and getNextPageParam
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+[`UseLiveInfiniteQueryReturn`](../../type-aliases/UseLiveInfiniteQueryReturn.md)\<`TContext`\>
+
+Object with pages, data, and pagination controls
+
+### Examples
+
+```ts
+// Basic infinite query
+const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
+ (q) => q
+ .from({ posts: postsCollection })
+ .orderBy(({ posts }) => posts.createdAt, 'desc')
+ .select(({ posts }) => ({
+ id: posts.id,
+ title: posts.title
+ })),
+ {
+ pageSize: 20,
+ getNextPageParam: (lastPage, allPages) =>
+ lastPage.length === 20 ? allPages.length : undefined
+ }
+)
+```
+
+```ts
+// With dependencies
+const { pages, fetchNextPage } = useLiveInfiniteQuery(
+ (q) => q
+ .from({ posts: postsCollection })
+ .where(({ posts }) => eq(posts.category, category))
+ .orderBy(({ posts }) => posts.createdAt, 'desc'),
+ {
+ pageSize: 10,
+ getNextPageParam: (lastPage) =>
+ lastPage.length === 10 ? lastPage.length : undefined
+ },
+ [category]
+)
+```
+
+```ts
+// Router loader pattern with pre-created collection
+// In loader:
+const postsQuery = createLiveQueryCollection({
+ query: (q) => q
+ .from({ posts: postsCollection })
+ .orderBy(({ posts }) => posts.createdAt, 'desc')
+ .limit(20)
+})
+await postsQuery.preload()
+return { postsQuery }
+
+// In component:
+const { postsQuery } = useLoaderData()
+const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
+ postsQuery,
+ {
+ pageSize: 20,
+ getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
+ }
+)
+```
diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md
new file mode 100644
index 000000000..84162b674
--- /dev/null
+++ b/docs/framework/react/reference/functions/useLiveQuery.md
@@ -0,0 +1,1251 @@
+---
+id: useLiveQuery
+title: useLiveQuery
+---
+
+# Function: useLiveQuery()
+
+## Call Signature
+
+```ts
+function useLiveQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:84](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L84)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) => `QueryBuilder`\<`TContext`\>
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection: Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}>;
+```
+
+#### data
+
+```ts
+data: InferResultType;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: true;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+#### status
+
+```ts
+status: CollectionStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:101](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L101)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) => `QueryBuilder`\<`TContext`\> \| `null` \| `undefined`
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection:
+ | Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>
+ | undefined;
+```
+
+#### data
+
+```ts
+data: InferResultType | undefined;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: boolean;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state:
+ | Map
+ | undefined;
+```
+
+#### status
+
+```ts
+status: UseLiveQueryStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:120](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L120)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) =>
+ \| `LiveQueryCollectionConfig`\<`TContext`, \{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \} & `object`\>
+ \| `null`
+ \| `undefined`
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection:
+ | Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>
+ | undefined;
+```
+
+#### data
+
+```ts
+data: InferResultType | undefined;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: boolean;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state:
+ | Map
+ | undefined;
+```
+
+#### status
+
+```ts
+status: UseLiveQueryStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:139](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L139)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### queryFn
+
+(`q`) =>
+ \| `Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\>
+ \| `null`
+ \| `undefined`
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection:
+ | Collection, TResult>
+ | undefined;
+```
+
+#### data
+
+```ts
+data: TResult[] | undefined;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: boolean;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state: Map | undefined;
+```
+
+#### status
+
+```ts
+status: UseLiveQueryStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:162](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L162)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### queryFn
+
+(`q`) =>
+ \| `QueryBuilder`\<`TContext`\>
+ \| `LiveQueryCollectionConfig`\<`TContext`, \{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \} & `object`\>
+ \| `Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\>
+ \| `null`
+ \| `undefined`
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection:
+ | Collection, TResult>
+ | Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>
+ | undefined;
+```
+
+#### data
+
+```ts
+data: InferResultType | TResult[] | undefined;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: boolean;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state:
+ | Map
+ | Map
+ | undefined;
+```
+
+#### status
+
+```ts
+status: UseLiveQueryStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(config, deps?): object;
+```
+
+Defined in: [useLiveQuery.ts:230](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L230)
+
+Create a live query using configuration object
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### config
+
+`LiveQueryCollectionConfig`\<`TContext`\>
+
+Configuration object with query and options
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection: Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}>;
+```
+
+#### data
+
+```ts
+data: InferResultType;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: true;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+#### status
+
+```ts
+status: CollectionStatus;
+```
+
+### Examples
+
+```ts
+// Basic config object usage
+const { data, status } = useLiveQuery({
+ query: (q) => q.from({ todos: todosCollection }),
+ gcTime: 60000
+})
+```
+
+```ts
+// With query builder and options
+const queryBuilder = new Query()
+ .from({ persons: collection })
+ .where(({ persons }) => gt(persons.age, 30))
+ .select(({ persons }) => ({ id: persons.id, name: persons.name }))
+
+const { data, isReady } = useLiveQuery({ query: queryBuilder })
+```
+
+```ts
+// Handle all states uniformly
+const { data, isLoading, isReady, isError } = useLiveQuery({
+ query: (q) => q.from({ items: itemCollection })
+})
+
+if (isLoading) return Loading...
+if (isError) return Something went wrong
+if (!isReady) return Preparing...
+
+return {data.length} items loaded
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(liveQueryCollection): object;
+```
+
+Defined in: [useLiveQuery.ts:276](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L276)
+
+Subscribe to an existing live query collection
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult`
+
+Pre-created live query collection to subscribe to
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection: Collection;
+```
+
+#### data
+
+```ts
+data: TResult[];
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: true;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+#### status
+
+```ts
+status: CollectionStatus;
+```
+
+### Examples
+
+```ts
+// Using pre-created live query collection
+const myLiveQuery = createLiveQueryCollection((q) =>
+ q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
+)
+const { data, collection } = useLiveQuery(myLiveQuery)
+```
+
+```ts
+// Access collection methods directly
+const { data, collection, isReady } = useLiveQuery(existingCollection)
+
+// Use collection for mutations
+const handleToggle = (id) => {
+ collection.update(id, draft => { draft.completed = !draft.completed })
+}
+```
+
+```ts
+// Handle states consistently
+const { data, isLoading, isError } = useLiveQuery(sharedCollection)
+
+if (isLoading) return Loading...
+if (isError) return Error loading data
+
+return {data.map(item => )}
+```
+
+## Call Signature
+
+```ts
+function useLiveQuery(liveQueryCollection): object;
+```
+
+Defined in: [useLiveQuery.ts:296](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L296)
+
+Create a live query using a query function
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `SingleResult`
+
+### Returns
+
+`object`
+
+Object with reactive data, state, and status information
+
+#### collection
+
+```ts
+collection: Collection, TResult> & SingleResult;
+```
+
+#### data
+
+```ts
+data: TResult | undefined;
+```
+
+#### isCleanedUp
+
+```ts
+isCleanedUp: boolean;
+```
+
+#### isEnabled
+
+```ts
+isEnabled: true;
+```
+
+#### isError
+
+```ts
+isError: boolean;
+```
+
+#### isIdle
+
+```ts
+isIdle: boolean;
+```
+
+#### isLoading
+
+```ts
+isLoading: boolean;
+```
+
+#### isReady
+
+```ts
+isReady: boolean;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+#### status
+
+```ts
+status: CollectionStatus;
+```
+
+### Examples
+
+```ts
+// Basic query with object syntax
+const { data, isLoading } = useLiveQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+)
+```
+
+```ts
+// Single result query
+const { data } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+```
+
+```ts
+// With dependencies that trigger re-execution
+const { data, state } = useLiveQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-run when minPriority changes
+)
+```
+
+```ts
+// Join pattern
+const { data } = useLiveQuery((q) =>
+ q.from({ issues: issueCollection })
+ .join({ persons: personCollection }, ({ issues, persons }) =>
+ eq(issues.userId, persons.id)
+ )
+ .select(({ issues, persons }) => ({
+ id: issues.id,
+ title: issues.title,
+ userName: persons.name
+ }))
+)
+```
+
+```ts
+// Handle loading and error states
+const { data, isLoading, isError, status } = useLiveQuery((q) =>
+ q.from({ todos: todoCollection })
+)
+
+if (isLoading) return Loading...
+if (isError) return Error: {status}
+
+return (
+
+ {data.map(todo => {todo.text} )}
+
+)
+```
diff --git a/docs/framework/react/reference/functions/useLiveSuspenseQuery.md b/docs/framework/react/reference/functions/useLiveSuspenseQuery.md
new file mode 100644
index 000000000..679fb98fd
--- /dev/null
+++ b/docs/framework/react/reference/functions/useLiveSuspenseQuery.md
@@ -0,0 +1,490 @@
+---
+id: useLiveSuspenseQuery
+title: useLiveSuspenseQuery
+---
+
+# Function: useLiveSuspenseQuery()
+
+## Call Signature
+
+```ts
+function useLiveSuspenseQuery(queryFn, deps?): object;
+```
+
+Defined in: [useLiveSuspenseQuery.ts:76](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L76)
+
+Create a live query with React Suspense support
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### queryFn
+
+(`q`) => `QueryBuilder`\<`TContext`\>
+
+Query function that defines what data to fetch
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data and state - data is guaranteed to be defined
+
+#### collection
+
+```ts
+collection: Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}>;
+```
+
+#### data
+
+```ts
+data: InferResultType;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+### Throws
+
+Promise when data is loading (caught by Suspense boundary)
+
+### Throws
+
+Error when collection fails (caught by Error boundary)
+
+### Examples
+
+```ts
+// Basic usage with Suspense
+function TodoList() {
+ const { data } = useLiveSuspenseQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+
+function App() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+```ts
+// Single result query
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+// data is guaranteed to be the single item (or undefined if not found)
+```
+
+```ts
+// With dependencies that trigger re-suspension
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-suspends when minPriority changes
+)
+```
+
+```ts
+// With Error boundary
+function App() {
+ return (
+ Error loading data}>
+ Loading...}>
+
+
+
+ )
+}
+```
+
+## Call Signature
+
+```ts
+function useLiveSuspenseQuery(config, deps?): object;
+```
+
+Defined in: [useLiveSuspenseQuery.ts:86](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L86)
+
+Create a live query with React Suspense support
+
+### Type Parameters
+
+#### TContext
+
+`TContext` *extends* `Context`
+
+### Parameters
+
+#### config
+
+`LiveQueryCollectionConfig`\<`TContext`\>
+
+#### deps?
+
+`unknown`[]
+
+Array of dependencies that trigger query re-execution when changed
+
+### Returns
+
+`object`
+
+Object with reactive data and state - data is guaranteed to be defined
+
+#### collection
+
+```ts
+collection: Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, {
+}>;
+```
+
+#### data
+
+```ts
+data: InferResultType;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+### Throws
+
+Promise when data is loading (caught by Suspense boundary)
+
+### Throws
+
+Error when collection fails (caught by Error boundary)
+
+### Examples
+
+```ts
+// Basic usage with Suspense
+function TodoList() {
+ const { data } = useLiveSuspenseQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+
+function App() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+```ts
+// Single result query
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+// data is guaranteed to be the single item (or undefined if not found)
+```
+
+```ts
+// With dependencies that trigger re-suspension
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-suspends when minPriority changes
+)
+```
+
+```ts
+// With Error boundary
+function App() {
+ return (
+ Error loading data}>
+ Loading...}>
+
+
+
+ )
+}
+```
+
+## Call Signature
+
+```ts
+function useLiveSuspenseQuery(liveQueryCollection): object;
+```
+
+Defined in: [useLiveSuspenseQuery.ts:96](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L96)
+
+Create a live query with React Suspense support
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult`
+
+### Returns
+
+`object`
+
+Object with reactive data and state - data is guaranteed to be defined
+
+#### collection
+
+```ts
+collection: Collection;
+```
+
+#### data
+
+```ts
+data: TResult[];
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+### Throws
+
+Promise when data is loading (caught by Suspense boundary)
+
+### Throws
+
+Error when collection fails (caught by Error boundary)
+
+### Examples
+
+```ts
+// Basic usage with Suspense
+function TodoList() {
+ const { data } = useLiveSuspenseQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+
+function App() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+```ts
+// Single result query
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+// data is guaranteed to be the single item (or undefined if not found)
+```
+
+```ts
+// With dependencies that trigger re-suspension
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-suspends when minPriority changes
+)
+```
+
+```ts
+// With Error boundary
+function App() {
+ return (
+ Error loading data}>
+ Loading...}>
+
+
+
+ )
+}
+```
+
+## Call Signature
+
+```ts
+function useLiveSuspenseQuery(liveQueryCollection): object;
+```
+
+Defined in: [useLiveSuspenseQuery.ts:109](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L109)
+
+Create a live query with React Suspense support
+
+### Type Parameters
+
+#### TResult
+
+`TResult` *extends* `object`
+
+#### TKey
+
+`TKey` *extends* `string` \| `number`
+
+#### TUtils
+
+`TUtils` *extends* `Record`\<`string`, `any`\>
+
+### Parameters
+
+#### liveQueryCollection
+
+`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `SingleResult`
+
+### Returns
+
+`object`
+
+Object with reactive data and state - data is guaranteed to be defined
+
+#### collection
+
+```ts
+collection: Collection, TResult> & SingleResult;
+```
+
+#### data
+
+```ts
+data: TResult | undefined;
+```
+
+#### state
+
+```ts
+state: Map;
+```
+
+### Throws
+
+Promise when data is loading (caught by Suspense boundary)
+
+### Throws
+
+Error when collection fails (caught by Error boundary)
+
+### Examples
+
+```ts
+// Basic usage with Suspense
+function TodoList() {
+ const { data } = useLiveSuspenseQuery((q) =>
+ q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.completed, false))
+ .select(({ todos }) => ({ id: todos.id, text: todos.text }))
+ )
+
+ return (
+
+ {data.map(todo => {todo.text} )}
+
+ )
+}
+
+function App() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+```ts
+// Single result query
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => eq(todos.id, 1))
+ .findOne()
+)
+// data is guaranteed to be the single item (or undefined if not found)
+```
+
+```ts
+// With dependencies that trigger re-suspension
+const { data } = useLiveSuspenseQuery(
+ (q) => q.from({ todos: todosCollection })
+ .where(({ todos }) => gt(todos.priority, minPriority)),
+ [minPriority] // Re-suspends when minPriority changes
+)
+```
+
+```ts
+// With Error boundary
+function App() {
+ return (
+ Error loading data}>
+ Loading...}>
+
+
+
+ )
+}
+```
diff --git a/docs/framework/react/reference/functions/usePacedMutations.md b/docs/framework/react/reference/functions/usePacedMutations.md
new file mode 100644
index 000000000..4f50b0411
--- /dev/null
+++ b/docs/framework/react/reference/functions/usePacedMutations.md
@@ -0,0 +1,131 @@
+---
+id: usePacedMutations
+title: usePacedMutations
+---
+
+# Function: usePacedMutations()
+
+```ts
+function usePacedMutations(config): (variables) => Transaction;
+```
+
+Defined in: [usePacedMutations.ts:93](https://github.com/TanStack/db/blob/main/packages/react-db/src/usePacedMutations.ts#L93)
+
+React hook for managing paced mutations with timing strategies.
+
+Provides optimistic mutations with pluggable strategies like debouncing,
+queuing, or throttling. The optimistic updates are applied immediately via
+`onMutate`, and the actual persistence is controlled by the strategy.
+
+## Type Parameters
+
+### TVariables
+
+`TVariables` = `unknown`
+
+### T
+
+`T` *extends* `object` = `Record`\<`string`, `unknown`\>
+
+## Parameters
+
+### config
+
+`PacedMutationsConfig`\<`TVariables`, `T`\>
+
+Configuration including onMutate, mutationFn and strategy
+
+## Returns
+
+A mutate function that accepts variables and returns Transaction objects
+
+```ts
+(variables): Transaction;
+```
+
+### Parameters
+
+#### variables
+
+`TVariables`
+
+### Returns
+
+`Transaction`\<`T`\>
+
+## Examples
+
+```tsx
+// Debounced auto-save
+function AutoSaveForm({ formId }: { formId: string }) {
+ const mutate = usePacedMutations({
+ onMutate: (value) => {
+ // Apply optimistic update immediately
+ formCollection.update(formId, draft => {
+ draft.content = value
+ })
+ },
+ mutationFn: async ({ transaction }) => {
+ await api.save(transaction.mutations)
+ },
+ strategy: debounceStrategy({ wait: 500 })
+ })
+
+ const handleChange = async (value: string) => {
+ const tx = mutate(value)
+
+ // Optional: await persistence or handle errors
+ try {
+ await tx.isPersisted.promise
+ console.log('Saved!')
+ } catch (error) {
+ console.error('Save failed:', error)
+ }
+ }
+
+ return