Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/add-supabase-db-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/supabase-db-collection': minor
---

Add `@tanstack/supabase-db-collection`, a collection that syncs TanStack DB with Supabase through PostgREST queries, optimistic mutations, and Realtime subscriptions. It replaces `@supabase-labs/tanstack-db@0.1.0` ([supabase/tanstack-db@3503c451](https://github.com/supabase/tanstack-db/commit/3503c451b2f2550ab4069067673881f39342b588)).

Compared with that package, it adds a `syncMode` option (`'on-demand'` by default, or `'eager'` to load the whole table), `parse` and `serialize` column conversions, a `defaultStringCollation` option, and materialization from a `DbClient`. Composite collection keys are now JSON arrays. `queryOnce` is now `experimental_queryOnce`, which runs only `count`, `sum`, and `avg` of plain columns on the server and always resolves to TanStack DB's `queryOnce` result.
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@tanstack/react-router-with-db": "1.0.0-rc.0",
"@tanstack/rxdb-db-collection": "1.0.0-rc.0",
"@tanstack/solid-db": "1.0.0-rc.0",
"@tanstack/supabase-db-collection": "1.0.0-rc.0",
"@tanstack/svelte-db": "1.0.0-rc.0",
"@tanstack/tauri-db-sqlite-persistence": "1.0.0-rc.0",
"@tanstack/tauri-db-sqlite-persistence-e2e-app": "0.0.49",
Expand Down
26 changes: 25 additions & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
group: [browser, desktop, cloud-mobile]
group: [browser, desktop, cloud-mobile, supabase]
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
Expand Down Expand Up @@ -76,6 +76,25 @@ jobs:
--filter '@tanstack/react-native-db-sqlite-persistence...'
build

- name: Setup Supabase CLI
if: matrix.group == 'supabase'
uses: supabase/setup-cli@689adcfb80d947100a1eab127289b97d1fb07b13 # v3.2.0
with:
version: 2.116.0

- name: Start Supabase
if: matrix.group == 'supabase'
# The collection needs only the database, PostgREST, Realtime and the
# API gateway (Kong). Skipping the other services saves image pulls and
# startup time. The e2e global setup reuses this running stack.
working-directory: packages/supabase-db-collection
run: supabase start -x studio,mailpit,storage-api,imgproxy,postgres-meta,edge-runtime,logflare,vector,supavisor

- name: Run Supabase E2E tests
if: matrix.group == 'supabase'
working-directory: packages/supabase-db-collection
run: pnpm test:e2e

- name: Run Electric E2E tests
if: matrix.group == 'browser'
run: |
Expand Down Expand Up @@ -233,6 +252,11 @@ jobs:
cd packages/db-collection-e2e/docker
docker compose down -v

- name: Stop Supabase
if: always() && matrix.group == 'supabase'
working-directory: packages/supabase-db-collection
run: supabase stop --no-backup

- name: Upload test results
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
Expand Down
6 changes: 5 additions & 1 deletion _artifacts/skill_tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ skills:
description: >
Creating typed collections with createCollection, queryCollectionOptions,
electricCollectionOptions, powerSyncCollectionOptions, rxdbCollectionOptions,
trailbaseCollectionOptions, localOnlyCollectionOptions, localStorageCollectionOptions.
trailbaseCollectionOptions, supabaseCollectionOptions, localOnlyCollectionOptions,
localStorageCollectionOptions.
CollectionConfig (getKey, schema, sync, gcTime, autoIndex, syncMode).
StandardSchema validation with Zod/Valibot/ArkType. Collection lifecycle
(idle/loading/ready/error/cleaned-up). Adapter-specific sync patterns.
Expand All @@ -49,6 +50,7 @@ skills:
- 'TanStack/db:docs/collections/powersync-collection.md'
- 'TanStack/db:docs/collections/rxdb-collection.md'
- 'TanStack/db:docs/collections/trailbase-collection.md'
- 'TanStack/db:docs/collections/supabase-collection.md'
- 'TanStack/db:docs/collections/local-only-collection.md'
- 'TanStack/db:docs/collections/local-storage-collection.md'
- 'TanStack/db:packages/db/src/collection/index.ts'
Expand All @@ -58,6 +60,7 @@ skills:
- 'PowerSync adapter'
- 'RxDB adapter'
- 'TrailBase adapter'
- 'Supabase adapter'
- 'Local-only'
- 'localStorage'
references:
Expand All @@ -66,6 +69,7 @@ skills:
- 'references/powersync-adapter.md'
- 'references/rxdb-adapter.md'
- 'references/trailbase-adapter.md'
- 'references/supabase-adapter.md'
- 'references/local-adapters.md'
- 'references/schema-patterns.md'

Expand Down
457 changes: 457 additions & 0 deletions docs/collections/supabase-collection.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
"label": "TrailBase Collection",
"to": "collections/trailbase-collection"
},
{
"label": "Supabase Collection",
"to": "collections/supabase-collection"
},
{
"label": "RxDB Collection",
"to": "collections/rxdb-collection"
Expand Down Expand Up @@ -217,6 +221,18 @@
{
"label": "indexedDBCollectionOptions",
"to": "reference/functions/indexedDBCollectionOptions"
},
{
"label": "Supabase DB Collection",
"to": "reference/supabase-db-collection/index"
},
{
"label": "supabaseCollectionOptions",
"to": "reference/supabase-db-collection/functions/supabaseCollectionOptions"
},
{
"label": "experimental_queryOnce",
"to": "reference/supabase-db-collection/functions/experimental_queryOnce"
}
],
"frameworks": [
Expand Down
15 changes: 14 additions & 1 deletion docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ npm install @tanstack/trailbase-db-collection

Use `trailBaseCollectionOptions` to sync records from TrailBase's Record APIs with built-in subscription support.

#### Supabase Collection

For syncing with [Supabase](https://supabase.com) tables through PostgREST and Realtime:

```sh
npm install @tanstack/supabase-db-collection @tanstack/query-core @supabase/supabase-js
```

Use `supabaseCollectionOptions` to load rows from Supabase tables, apply optimistic mutations, and receive changes from other clients through Realtime. Row Level Security is respected.

#### PowerSync Collection

For offline-first sync with [PowerSync](https://powersync.com):
Expand Down
4 changes: 3 additions & 1 deletion docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ TanStack DB provides several built-in collection types for different data source

- **[TrailBaseCollection](./collections/trailbase-collection.md)** — Sync data into collections using TrailBase's self-hosted backend with real-time subscriptions.

- **[SupabaseCollection](./collections/supabase-collection.md)** — Sync data into collections from Supabase tables using PostgREST queries and Realtime subscriptions, respecting Row Level Security.

- **[RxDBCollection](./collections/rxdb-collection.md)** — Integrate with RxDB for offline-first local persistence with powerful replication and sync capabilities.

- **[PowerSyncCollection](./collections/powersync-collection.md)** — Sync with PowerSync's SQLite-based database for offline-first persistence with real-time synchronization with PostgreSQL, MongoDB, and MySQL backends.
Expand Down Expand Up @@ -291,7 +293,7 @@ The collection will use the schema for its type inference. If you provide a sche

You can create your own collection types by implementing the `Collection` interface found in [`../packages/db/src/collection/index.ts`](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts).

See the existing implementations in [`../packages/db`](https://github.com/TanStack/db/tree/main/packages/db), [`../packages/query-db-collection`](https://github.com/TanStack/db/tree/main/packages/query-db-collection), [`../packages/electric-db-collection`](https://github.com/TanStack/db/tree/main/packages/electric-db-collection), [`../packages/trailbase-db-collection`](https://github.com/TanStack/db/tree/main/packages/trailbase-db-collection) and [`../packages/powersync-db-collection`](https://github.com/TanStack/db/tree/main/packages/powersync-db-collection) for reference. Also see the [Collection Options Creator guide](./guides/collection-options-creator.md) for a pattern to create reusable collection configuration factories.
See the existing implementations in [`../packages/db`](https://github.com/TanStack/db/tree/main/packages/db), [`../packages/query-db-collection`](https://github.com/TanStack/db/tree/main/packages/query-db-collection), [`../packages/electric-db-collection`](https://github.com/TanStack/db/tree/main/packages/electric-db-collection), [`../packages/trailbase-db-collection`](https://github.com/TanStack/db/tree/main/packages/trailbase-db-collection), [`../packages/supabase-db-collection`](https://github.com/TanStack/db/tree/main/packages/supabase-db-collection) and [`../packages/powersync-db-collection`](https://github.com/TanStack/db/tree/main/packages/powersync-db-collection) for reference. Also see the [Collection Options Creator guide](./guides/collection-options-creator.md) for a pattern to create reusable collection configuration factories.

### Live queries

Expand Down
4 changes: 4 additions & 0 deletions domain_map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ skills:
- 'queryCollectionOptions (@tanstack/query-db-collection)'
- 'electricCollectionOptions (@tanstack/electric-db-collection)'
- 'trailbaseCollectionOptions (@tanstack/trailbase-db-collection)'
- 'supabaseCollectionOptions (@tanstack/supabase-db-collection)'
- 'powerSyncCollectionOptions (@tanstack/powersync-db-collection)'
- 'rxdbCollectionOptions (@tanstack/rxdb-db-collection)'
- 'localOnlyCollectionOptions'
Expand Down Expand Up @@ -92,6 +93,9 @@ skills:
- name: 'TrailBase adapter'
package: '@tanstack/trailbase-db-collection'
config_surface: 'recordApi, conversions'
- name: 'Supabase adapter'
package: '@tanstack/supabase-db-collection'
config_surface: 'tableName, keys, schema, supabase, queryClient, realtime, realtimeUseFilter, syncMode (on-demand default/eager), defaultStringCollation, parse, serialize'
- name: 'Local-only'
package: '@tanstack/db'
config_surface: 'getKey, schema, initialData'
Expand Down
3 changes: 2 additions & 1 deletion packages/db/skills/db-core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: db-core
description: >
TanStack DB core concepts: createCollection with queryCollectionOptions,
electricCollectionOptions, powerSyncCollectionOptions, rxdbCollectionOptions,
trailBaseCollectionOptions, localOnlyCollectionOptions. Live queries via
trailBaseCollectionOptions, supabaseCollectionOptions,
localOnlyCollectionOptions. Live queries via
query builder (from, where, join, select, groupBy, orderBy, limit). Optimistic
mutations with draft proxy (collection.insert, collection.update,
collection.delete). createOptimisticAction, createTransaction,
Expand Down
7 changes: 5 additions & 2 deletions packages/db/skills/db-core/collection-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: >
queryCollectionOptions (REST/TanStack Query), electricCollectionOptions
(ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),
rxdbCollectionOptions (RxDB), trailBaseCollectionOptions (TrailBase),
localOnlyCollectionOptions, localStorageCollectionOptions,
indexedDBCollectionOptions. CollectionConfig
supabaseCollectionOptions (Supabase), localOnlyCollectionOptions,
localStorageCollectionOptions, indexedDBCollectionOptions. CollectionConfig
options: getKey, schema, sync, gcTime, autoIndex (default off), defaultIndexType,
syncMode (eager/on-demand, plus progressive for Electric). StandardSchema validation
with Zod/Valibot/ArkType. Collection lifecycle (idle/loading/ready/error).
Expand All @@ -25,6 +25,7 @@ sources:
- 'TanStack/db:docs/collections/rxdb-collection.md'
- 'TanStack/db:docs/collections/trailbase-collection.md'
- 'TanStack/db:docs/collections/indexed-db-collection.md'
- 'TanStack/db:docs/collections/supabase-collection.md'
- 'TanStack/db:packages/db/src/collection/index.ts'
---

Expand Down Expand Up @@ -87,6 +88,7 @@ const todoCollection = createCollection(
| PowerSync (SQLite offline) | `powerSyncCollectionOptions` | `@tanstack/powersync-db-collection` |
| RxDB (reactive database) | `rxdbCollectionOptions` | `@tanstack/rxdb-db-collection` |
| TrailBase (event streaming) | `trailBaseCollectionOptions` | `@tanstack/trailbase-db-collection` |
| Supabase (Postgres + Realtime) | `supabaseCollectionOptions` | `@tanstack/supabase-db-collection` |
| No backend (UI state) | `localOnlyCollectionOptions` | `@tanstack/db` |
| Browser localStorage | `localStorageCollectionOptions` | `@tanstack/db` |
| Browser IndexedDB | `indexedDBCollectionOptions` | `@tanstack/db` |
Expand Down Expand Up @@ -464,6 +466,7 @@ Source: docs/collections/query-collection.md
- [PowerSync adapter](references/powersync-adapter.md)
- [RxDB adapter](references/rxdb-adapter.md)
- [TrailBase adapter](references/trailbase-adapter.md)
- [Supabase adapter](references/supabase-adapter.md)
- [Local adapters (local-only, localStorage)](references/local-adapters.md)
- [Schema validation patterns](references/schema-patterns.md)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Supabase Adapter Reference

## Install

```bash
pnpm add @tanstack/supabase-db-collection @tanstack/react-db @tanstack/query-core @supabase/supabase-js
```

## Required Config

```typescript
import { createCollection } from '@tanstack/react-db'
import { supabaseCollectionOptions } from '@tanstack/supabase-db-collection'
import { createClient } from '@supabase/supabase-js'
import { z } from 'zod'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

const todoSchema = z.object({
id: z.string(),
title: z.string(),
completed: z.boolean(),
})

const todosCollection = createCollection(
supabaseCollectionOptions({
tableName: 'todos',
keys: ['id'],
schema: todoSchema,
supabase,
}),
)
```

- `tableName` -- Postgres table name (PostgREST endpoint); also used as the collection id
- `keys` -- column(s) that uniquely identify a row; match the primary key(s). The collection key is the column value as a string (`6` becomes `'6'`); composite keys are a JSON array of the string values, such as `'["a","b"]'`. Pass that key to `get`, `update`, and `delete`
- `schema` -- Standard Schema (Zod, Valibot, ...) describing one row
- `supabase` -- `SupabaseClient` used for queries, mutations, and Realtime

## Optional Config

| Option | Default | Description |
| ------------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `syncMode` | `'on-demand'` | `'on-demand'` loads only what live queries need (pushes where/orderBy/limit to PostgREST); `'eager'` loads the whole table, paging past `db-max-rows` |
| `realtime` | `false` | Subscribe to Postgres changes and reconcile other clients' inserts/updates/deletes |
| `realtimeUseFilter` | `false` | Experimental. Push each query's WHERE to Realtime as a `postgres_changes` filter to cut message volume |
| `queryClient` | shared client | TanStack Query client used for the cache. Shared in the browser, new per call on the server; a `DbClient`'s `queryClient` dependency takes precedence |
| `defaultStringCollation` | TanStack DB default | `StringCollationConfig` from `@tanstack/db`; orders strings to match the database collation |
| `parse` / `serialize` | none | Per-column converters between PostgREST JSON and the schema type (e.g. `timestamptz` string to `Date`); skipped for `null` |

## Real-time Subscriptions

Add the table to the `supabase_realtime` publication first:

```sql
alter publication supabase_realtime add table "public"."todos";
```

Then set `realtime: true`. RLS applies to Realtime. Without RLS every change is broadcast to every client. Realtime can consume more messages than expected; test with a spend cap.

## Persistence Handlers

The adapter owns `onInsert`, `onUpdate`, and `onDelete`: they write through PostgREST (`POST`/`PATCH`/`DELETE`, matching on the `keys` columns), write the server-returned row back into the collection, and roll back the optimistic change if the server rejects it. Custom mutation handlers are not part of the options.

## Query Pushdown

Pushed to PostgREST: `where` (`eq`, `gt`, `gte`, `lt`, `lte`, `inArray`, `like`, `ilike`, `not`, `isNull`, `and`, `or`), `orderBy` on source columns, `limit`, and join keys (`in` filter). Evaluated client-side: computed selects, aggregates, `groupBy`, `having`, `distinct`, except an aggregate-only `count`/`sum`/`avg` in `experimental_queryOnce`. Reads page past PostgREST's `db-max-rows` (1000 by default) with `offset`; a multi-page read also orders by the `keys` columns so the pages form one sequence.

## experimental_queryOnce

```typescript
import { eq } from '@tanstack/react-db'
import { experimental_queryOnce } from '@tanstack/supabase-db-collection'

const completed = await experimental_queryOnce(
(q) =>
q
.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, true)),
supabase,
)
```

One-shot, non-reactive fetch. It always resolves to what TanStack DB's `queryOnce` returns, and it is experimental because it reads TanStack DB's internal query IR.

A query runs as one PostgREST request only when its select holds just `count`, `sum`, or `avg` of a plain column of one Supabase table (not `sum`/`avg` over a `parse` column), every filter can be pushed, and it has no `groupBy`, `having`, join, `orderBy`, `limit`, `offset`, `distinct`, or functional clause. That path needs PostgREST aggregates:

```sql
alter role authenticator set pgrst.db_aggregates_enabled = 'true';
notify pgrst, 'reload config';
```

Every other query, including `min`, `max`, `groupBy`, and `having`, runs through TanStack DB's `queryOnce` over the collections, which push filters, order, and limit to PostgREST. TanStack DB's aggregate rules apply: an empty match gives `[]` (`undefined` for `findOne`), and an all-null `sum` gives `0`.

## Use with DbClient

Under a `DbClient`, wrap the options in `collectionOptions` and materialize with `dbClient.collection`. The client's `queryClient` dependency is used for the cache.

```typescript
import { QueryClient } from '@tanstack/query-core'
import { DbClient, collectionOptions } from '@tanstack/db'

const todosDescriptor = collectionOptions(
supabaseCollectionOptions({
tableName: 'todos',
keys: ['id'],
schema: todoSchema,
supabase,
}),
)

const dbClient = new DbClient({ queryClient: new QueryClient() })
const todos = dbClient.collection(todosDescriptor)
```

## Complete Example

```tsx
import { createCollection, useLiveQuery, eq } from '@tanstack/react-db'
import { supabaseCollectionOptions } from '@tanstack/supabase-db-collection'

const todosCollection = createCollection(
supabaseCollectionOptions({
tableName: 'todos',
keys: ['id'],
schema: todoSchema,
supabase,
syncMode: 'on-demand',
realtime: true,
}),
)

function Todos() {
const { data } = useLiveQuery({
query: (q) =>
q
.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, false)),
})

const add = (title: string) =>
todosCollection.insert({ id: crypto.randomUUID(), title, completed: false })

const complete = (id: string) =>
todosCollection.update(id, (draft) => {
draft.completed = true
})

// ...
}
```
Loading