# TanStack DB

> TanStack DB collections that load through a query or through @supabase-labs/tanstack-db, and write through the repositories.

Source: https://bettersupabase.com/docs/frontend/tanstack-db

better-supabase has two ways to build a TanStack DB collection.
`better-supabase/tanstack-db/supabase` wraps Supabase's
[`@supabase-labs/tanstack-db`](https://www.npmjs.com/package/@supabase-labs/tanstack-db):
live queries load on demand through PostgREST and Realtime keeps them
current. `better-supabase/tanstack-db` loads a collection through one of
your [query options](/docs/frontend/query) instead, works with camel casing
and needs no other package.

## With @supabase-labs/tanstack-db [#with-supabase-labstanstack-db]

```bash
pnpm add @supabase-labs/tanstack-db @tanstack/react-db
```

```ts title="src/lib/collections.ts"
import { createCollection } from "@tanstack/react-db";
import { supabaseCollection } from "better-supabase/tanstack-db/supabase";
import { customersRow } from "./supabase/generated.standard";
import { betterSupabase } from "./supabase";
import { db, supabase } from "./client";

export const customers = createCollection(
  supabaseCollection(betterSupabase, supabase, "customers", {
    schema: customersRow,
    db,
    realtime: true,
  }),
);
```

`supabaseCollection(betterSupabase, supabase, table, options)` calls
`supabaseCollectionOptions()` with the table's database name and primary
key, then replaces its write handlers. `schema` is any Standard Schema for
the row: the `standardSchema()`, `zod()` or `valibot()` generator output
works. `realtime: true` subscribes to `postgres_changes` on the table, and
`realtimeUseFilter: true` narrows that subscription to each live query's
filter. Pass `queryClient` to share one with the rest of the app.

Inserts, updates and deletes run `create`, `update` and `delete` on the
table's repository, so RLS, hooks, events and plugins apply, and a failed
write throws a `DbException` that rolls the optimistic change back. The row
the repository returns goes straight into the collection. Tables with
[codec](/docs/concepts/temporal) columns refetch instead, because the
repository decodes values that the synced rows keep raw. Pass
`writes: "direct"` (and no `db`) to keep the package's own handlers, which
write through the supabase client.

The package reads and writes database column names and listens to the
`public` schema, so `supabaseCollection()` throws for a schema with
`casing: "camel"` and for tables in other schemas. Use
`collectionOptions()` for those.

For Realtime, add each table to the publication in a migration:

```sql title="supabase/schemas/realtime.sql"
alter publication supabase_realtime add table public.customers;
```

Delete events carry the replica identity of the deleted row, so the table
needs a primary key or `replica identity full`.
[`doctor`](/docs/cli/doctor#bs306) reports a published table whose delete
events carry no keys as BS306.

## Through a query option [#through-a-query-option]

```ts title="src/lib/collections.ts"
import { createCollection } from "@tanstack/react-db";
import { queryCollectionOptions } from "@tanstack/query-db-collection";
import { collectionOptions } from "better-supabase/tanstack-db";
import { betterSupabase } from "./supabase";
import { db, queries, queryClient } from "./client";

export const customers = createCollection(
  queryCollectionOptions(
    collectionOptions(betterSupabase, db, "customers", {
      query: queries.customers.findMany({ where: { status: "active" } }),
      queryClient,
    }),
  ),
);
```

`collectionOptions(betterSupabase, db, table, options)` returns the options
`queryCollectionOptions()` reads. The collection loads its rows with the
[query option](/docs/frontend/query) you pass and keys them by the table's
primary key (a JSON array for a composite key). TanStack DB is not a
dependency: the options are typed by shape.

## Writes [#writes]

`customers.insert(row)`, `customers.update(id, draft)` and
`customers.delete(id)` apply optimistically, then run `create`, `update` and
`delete` on the table's repository, so RLS, hooks, events and plugins apply.
A failed write throws a `DbException`, and TanStack DB rolls the optimistic
change back. After a write the collection refetches its query. To refresh
the other queries that read the table, call
`invalidateOnMutation(betterSupabase, queryClient)` from
`better-supabase/query` once at startup.

## Live queries [#live-queries]

Join and filter collections with TanStack DB's `useLiveQuery`. To refresh a
collection when another client changes the table, invalidate its tables
from a realtime subscription with `invalidateTables(queryClient, ["customers"])`.