db-core/collection-setup

Creating typed collections with createCollection. Adapter selection: queryCollectionOptions (REST/TanStack Query), electricCollectionOptions (ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite), rxdbCollectionOptions (RxDB), trailBaseCollectionOptions (TrailBase), localOnlyCollectionOptions, localStorageCollectionOptions. 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). Adapter-specific sync patterns including Electric txid tracking, Query direct writes, Query initial data and scoped factories, and PowerSync query-driven sync with onLoad/onLoadSubset hooks.

Install
npx skills add 'https://github.com/TanStack/db/tree/main/packages/db/skills/db-core/collection-setup'
Download bundle ↓
main · 09776a8Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
---name: db-core/collection-setupdescription: >  Creating typed collections with createCollection. Adapter selection:  queryCollectionOptions (REST/TanStack Query), electricCollectionOptions  (ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),  rxdbCollectionOptions (RxDB), trailBaseCollectionOptions (TrailBase),  localOnlyCollectionOptions, localStorageCollectionOptions. 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).  Adapter-specific sync patterns including Electric txid tracking, Query direct  writes, Query initial data and scoped factories, and PowerSync query-driven  sync with onLoad/onLoadSubset hooks.type: sub-skilllibrary: dblibrary_version: '0.6.17'sources:  - 'TanStack/db:docs/overview.md'  - 'TanStack/db:docs/guides/schemas.md'  - 'TanStack/db:docs/collections/query-collection.md'  - 'TanStack/db:docs/collections/electric-collection.md'  - 'TanStack/db:docs/collections/powersync-collection.md'  - 'TanStack/db:docs/collections/rxdb-collection.md'  - 'TanStack/db:docs/collections/trailbase-collection.md'  - 'TanStack/db:packages/db/src/collection/index.ts'--- This skill builds on db-core. Read it first for the overall mental model. # Collection Setup & Schema ## Setup ```tsimport { createCollection } from '@tanstack/react-db'import { queryCollectionOptions } from '@tanstack/query-db-collection'import { QueryClient } from '@tanstack/query-core'import { z } from 'zod' const queryClient = new QueryClient() const todoSchema = z.object({  id: z.number(),  text: z.string(),  completed: z.boolean().default(false),  created_at: z    .union([z.string(), z.date()])    .transform((val) => (typeof val === 'string' ? new Date(val) : val)),}) const todoCollection = createCollection(  queryCollectionOptions({    queryKey: ['todos'],    queryFn: async (ctx) => {      const res = await fetch('/api/todos', { signal: ctx.signal })      return res.json()    },    queryClient,    getKey: (item) => item.id,    schema: todoSchema,    onInsert: async ({ transaction }) => {      await api.todos.create(transaction.mutations[0].modified)    },    onUpdate: async ({ transaction }) => {      const mut = transaction.mutations[0]      await api.todos.update(mut.key, mut.changes)    },    onDelete: async ({ transaction }) => {      await api.todos.delete(transaction.mutations[0].key)    },  }),)``` ## Choosing an Adapter | Backend                          | Adapter                         | Package                             || -------------------------------- | ------------------------------- | ----------------------------------- || REST API / TanStack Query        | `queryCollectionOptions`        | `@tanstack/query-db-collection`     || ElectricSQL (real-time Postgres) | `electricCollectionOptions`     | `@tanstack/electric-db-collection`  || PowerSync (SQLite offline)       | `powerSyncCollectionOptions`    | `@tanstack/powersync-db-collection` || RxDB (reactive database)         | `rxdbCollectionOptions`         | `@tanstack/rxdb-db-collection`      || TrailBase (event streaming)      | `trailBaseCollectionOptions`    | `@tanstack/trailbase-db-collection` || No backend (UI state)            | `localOnlyCollectionOptions`    | `@tanstack/db`                      || Browser localStorage             | `localStorageCollectionOptions` | `@tanstack/db`                      | If the user specifies a backend (e.g. Electric, PowerSync), use that adapter directly. Only use `localOnlyCollectionOptions` when there is no backend yet — the collection API is uniform, so swapping to a real adapter later only changes the options creator. ## Sync Modes ```tsqueryCollectionOptions({  syncMode: 'eager', // default — loads all data upfront  // syncMode: "on-demand", // loads only what live queries request  // syncMode: "progressive", // (Electric only) query subset first, full sync in background})``` | Mode          | Best for                                                       | Data size || ------------- | -------------------------------------------------------------- | --------- || `eager`       | Mostly-static datasets                                         | <10k rows || `on-demand`   | Search, catalogs, large tables                                 | >50k rows || `progressive` | Collaborative apps needing instant first paint (Electric only) | Any       | Calling `collection.preload()` on an on-demand collection is a no-op. Createthe live query for the required subset and call `liveQuery.preload()` instead. For Query Collection request cancellation, cleanup boundaries, and shared`QueryClient` behavior, read[the Query adapter reference](references/query-adapter.md#request-cancellation-and-cleanup). ## Indexing Indexing is opt-in. The `autoIndex` option defaults to `"off"`. To enable automatic indexing, set `autoIndex: "eager"` and provide a `defaultIndexType`: ```tsimport { BasicIndex } from '@tanstack/db' createCollection(  queryCollectionOptions({    autoIndex: 'eager',    defaultIndexType: BasicIndex,    // ...  }),)``` Without `defaultIndexType`, setting `autoIndex: "eager"` throws a `CollectionConfigurationError`. You can also create indexes manually with `collection.createIndex()` and remove them with `collection.removeIndex()`. ## Core Patterns ### Local-only collection for prototyping ```tsimport {  createCollection,  localOnlyCollectionOptions,} from '@tanstack/react-db' const todoCollection = createCollection(  localOnlyCollectionOptions({    getKey: (item) => item.id,    initialData: [{ id: 1, text: 'Learn TanStack DB', completed: false }],  }),)``` ### Schema with type transformations ```tsconst schema = z.object({  id: z.number(),  title: z.string(),  due_date: z    .union([z.string(), z.date()])    .transform((val) => (typeof val === 'string' ? new Date(val) : val)),  priority: z.number().default(0),})``` Use `z.union([z.string(), z.date()])` for transformed fields — this ensures `TInput` is a superset of `TOutput` so that `update()` works correctly with the draft proxy. ### ElectricSQL with txid tracking Always use a schema with Electric — without one, the collection types as `Record<string, unknown>`. ```tsimport { electricCollectionOptions } from '@tanstack/electric-db-collection'import { z } from 'zod' const todoSchema = z.object({  id: z.string(),  text: z.string(),  completed: z.boolean(),  created_at: z.coerce.date(),}) const todoCollection = createCollection(  electricCollectionOptions({    schema: todoSchema,    shapeOptions: { url: '/api/electric/todos' },    getKey: (item) => item.id,    onInsert: async ({ transaction }) => {      const res = await api.todos.create(transaction.mutations[0].modified)      return { txid: res.txid }    },  }),)``` The returned `txid` tells the collection to hold optimistic state until Electric streams back that transaction. See the [Electric adapter reference](references/electric-adapter.md) for the full dual-path pattern (schema + parser). ## Common Mistakes ### CRITICAL queryFn returning empty array deletes all data Wrong: ```tsqueryCollectionOptions({  queryFn: async () => {    const res = await fetch('/api/todos?status=active')    return res.json() // returns [] when no active todos — deletes everything  },})``` Correct: ```tsqueryCollectionOptions({  queryFn: async () => {    const res = await fetch('/api/todos') // fetch complete state    return res.json()  },  // Use on-demand mode + live query where() for filtering  syncMode: 'on-demand',})``` In eager mode, `queryFn` is complete collection state. Returning `[]` means"the server has no items" and removes all rows. In on-demand mode, a result iscomplete only for that exact subset/Query key; an empty result releases thatsubset's ownership, while overlapping subsets can keep shared rows. Source: docs/collections/query-collection.md ### CRITICAL Not using the correct adapter for your backend Wrong: ```tsconst todoCollection = createCollection(  localOnlyCollectionOptions({    getKey: (item) => item.id,  }),)// Manually fetching and inserting...``` Correct: ```tsconst todoCollection = createCollection(  queryCollectionOptions({    queryKey: ['todos'],    queryFn: async () => fetch('/api/todos').then((r) => r.json()),    queryClient,    getKey: (item) => item.id,  }),)``` Each backend has a dedicated adapter that handles sync, mutation handlers, and utilities. Using `localOnlyCollectionOptions` or bare `createCollection` for a real backend bypasses all of this. Source: docs/overview.md ### CRITICAL Electric txid queried outside mutation transaction Wrong: ```ts// Backend handlerapp.post('/api/todos', async (req, res) => {  const txid = await generateTxId(sql) // WRONG: separate transaction  await sql`INSERT INTO todos ${sql(req.body)}`  res.json({ txid })})``` Correct: ```tsapp.post('/api/todos', async (req, res) => {  let txid  await sql.begin(async (tx) => {    txid = await generateTxId(tx) // CORRECT: same transaction    await tx`INSERT INTO todos ${tx(req.body)}`  })  res.json({ txid })})``` `pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` times out (default 5 seconds). Source: docs/collections/electric-collection.md ### CRITICAL queryFn returning partial data without merging Wrong: ```tsqueryCollectionOptions({  queryFn: async () => {    const newItems = await fetch('/api/todos?since=' + lastSync)    return newItems.json() // only new items — everything else deleted  },})``` Correct: ```tsqueryCollectionOptions({  queryFn: async (ctx) => {    const existing = ctx.queryClient.getQueryData(['todos']) || []    const newItems = await fetch('/api/todos?since=' + lastSync).then((r) =>      r.json(),    )    return [...existing, ...newItems]  },})``` An eager `queryFn` result replaces all collection data. For incremental eagerfetches, merge with existing data. In on-demand mode, return the complete statefor the requested subset instead. Source: docs/collections/query-collection.md ### HIGH Using async schema validation Wrong: ```tsconst schema = z.object({  email: z.string().refine(async (val) => {    const exists = await checkEmail(val)    return !exists  }),})``` Correct: ```tsconst schema = z.object({  email: z.string().email(),})// Do async validation in the mutation handler instead``` Schema validation must be synchronous. Async validation throws `SchemaMustBeSynchronousError` at mutation time. Source: packages/db/src/collection/mutations.ts:101 ### HIGH getKey returning undefined for some items Wrong: ```tscreateCollection(  queryCollectionOptions({    getKey: (item) => item.metadata.id, // undefined if metadata missing  }),)``` Correct: ```tscreateCollection(  queryCollectionOptions({    getKey: (item) => item.id, // always present  }),)``` `getKey` must return a defined value for every item. Throws `UndefinedKeyError` otherwise. Source: packages/db/src/collection/mutations.ts:148 ### HIGH TInput not a superset of TOutput with schema transforms Wrong: ```tsconst schema = z.object({  created_at: z.string().transform((val) => new Date(val)),})// update() fails — draft.created_at is Date but schema only accepts string``` Correct: ```tsconst schema = z.object({  created_at: z    .union([z.string(), z.date()])    .transform((val) => (typeof val === 'string' ? new Date(val) : val)),})``` When a schema transforms types, `TInput` must accept both the pre-transform and post-transform types for `update()` to work with the draft proxy. Source: docs/guides/schemas.md ### HIGH Runtime has no secure random number generator TanStack DB's `safeRandomUUID()` uses `crypto.randomUUID()` when available andfalls back to `crypto.getRandomValues()`, including on non-secure HTTP origins.Add a Web Crypto polyfill only in runtimes, including some React Nativeversions, that provide neither API. ```tsimport { safeRandomUUID } from '@tanstack/db' collection.insert({ id: safeRandomUUID(), text: 'New item' })``` Source: packages/db/src/utils/uuid.ts, packages/db/tests/uuid.test.ts ### MEDIUM Providing both explicit type parameter and schema Wrong: ```tscreateCollection<Todo>(queryCollectionOptions({ schema: todoSchema, ... }))``` Correct: ```tscreateCollection(queryCollectionOptions({ schema: todoSchema, ... }))``` When a schema is provided, the collection infers types from it. An explicit generic creates conflicting type constraints. Source: docs/overview.md ### MEDIUM Direct writes overridden by next query sync Wrong: ```tstodoCollection.utils.writeInsert(newItem)// Next queryFn execution replaces all data, losing the direct write``` Correct: ```tstodoCollection.utils.writeInsert(newItem)// Use staleTime to prevent immediate refetch// Or return { refetch: false } from mutation handlers``` Direct writes update the collection immediately, but the next `queryFn` returns complete server state which overwrites them. Source: docs/collections/query-collection.md ## References - [TanStack Query adapter](references/query-adapter.md)- [ElectricSQL adapter](references/electric-adapter.md)- [PowerSync adapter](references/powersync-adapter.md)- [RxDB adapter](references/rxdb-adapter.md)- [TrailBase adapter](references/trailbase-adapter.md)- [Local adapters (local-only, localStorage)](references/local-adapters.md)- [Schema validation patterns](references/schema-patterns.md) See also: db-core/mutations-optimistic/SKILL.md — mutation handlers configured here execute during mutations. See also: db-core/custom-adapter/SKILL.md — for building your own adapter. 
Discovery context

Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.