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
← Back to SKILL.md

PowerSync Adapter Reference

Install

pnpm add @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite

Required Config

import { createCollection, safeRandomUUID } from '@tanstack/react-db'
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'

const APP_SCHEMA = new Schema({
  documents: new Table({
    name: column.text,
    author: column.text,
    created_at: column.text,
    archived: column.integer,
  }),
})

const db = new PowerSyncDatabase({
  database: { dbFilename: 'app.sqlite' },
  schema: APP_SCHEMA,
})

const documentsCollection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.documents,
  }),
)
  • database -- PowerSyncDatabase instance
  • table -- PowerSync Table from schema (provides getKey and type inference)

Optional Config (with defaults)

OptionDefaultDescription
schema(none)StandardSchema for mutation validation
deserializationSchema(none)Transforms SQLite types to output types; required when input types differ from SQLite
onDeserializationError(none)Fatal error handler; required when using schema or deserializationSchema
serializer(none)Per-field functions to serialize output types back to SQLite
syncBatchSize1000Batch size for initial sync

SQLite Type Mapping

PowerSync ColumnTypeScript Type
column.textstring | null
column.integernumber | null
column.realnumber | null

All columns nullable by default. id: string is always included automatically.

Conversions (4 patterns)

1. Type Inference Only (no schema)

const collection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.documents,
  }),
)
// Input/Output: { id: string, name: string | null, created_at: string | null, ... }

2. Schema Validation (same SQLite types)

const schema = z.object({
  id: z.string(),
  name: z.string().min(3),
  author: z.string(),
  created_at: z.string(),
  archived: z.number(),
})
const collection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.documents,
    schema,
    onDeserializationError: (error) => {
      /* fatal */
    },
  }),
)

3. Transform SQLite to Rich Output Types

const schema = z.object({
  id: z.string(),
  name: z.string().nullable(),
  created_at: z
    .string()
    .nullable()
    .transform((val) => (val ? new Date(val) : null)),
  archived: z
    .number()
    .nullable()
    .transform((val) => (val != null ? val > 0 : null)),
})
const collection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.documents,
    schema,
    onDeserializationError: (error) => {
      /* fatal */
    },
    serializer: { created_at: (value) => (value ? value.toISOString() : null) },
  }),
)
// Input:  { created_at: string | null, ... }
// Output: { created_at: Date | null, archived: boolean | null, ... }

4. Custom Input + Output with deserializationSchema

const schema = z.object({
  id: z.string(),
  name: z.string(),
  created_at: z.date(),
  archived: z.boolean(),
})
const deserializationSchema = z.object({
  id: z.string(),
  name: z.string(),
  created_at: z.string().transform((val) => new Date(val)),
  archived: z.number().transform((val) => val > 0),
})
const collection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.documents,
    schema,
    deserializationSchema,
    onDeserializationError: (error) => {
      /* fatal */
    },
  }),
)
// Input:  { created_at: Date, archived: boolean }
// Output: { created_at: Date, archived: boolean }

Metadata Tracking

Enable on the table, then pass metadata with operations:

const APP_SCHEMA = new Schema({
  documents: new Table({ name: column.text }, { trackMetadata: true }),
})

await collection.insert(
  { id: safeRandomUUID(), name: 'Report' },
  { metadata: { source: 'web-app', userId: 'user-123' } },
).isPersisted.promise

Metadata appears as entry.metadata (stringified JSON) in PowerSync CrudEntry.

Advanced Transactions

import { createTransaction } from '@tanstack/react-db'
import { PowerSyncTransactor } from '@tanstack/powersync-db-collection'

const tx = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    await new PowerSyncTransactor({ database: db }).applyTransaction(
      transaction,
    )
  },
})
tx.mutate(() => {
  documentsCollection.insert({
    id: safeRandomUUID(),
    name: 'Doc 1',
    created_at: new Date().toISOString(),
  })
})
await tx.commit()
await tx.isPersisted.promise

On-Demand Sync Mode

PowerSync supports on-demand sync mode (query-driven sync), where only rows matching active live query predicates are loaded from SQLite into the collection. This can be combined with Sync Streams via onLoad (eager) or onLoadSubset (on-demand) hooks to also control which data the PowerSync Service syncs to the device. Use extractSimpleComparisons or parseWhereExpression to derive Sync Stream parameters dynamically from live query predicates.

Complete Example

import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'
import { createCollection } from '@tanstack/react-db'
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
import { z } from 'zod'

const APP_SCHEMA = new Schema({
  tasks: new Table({
    title: column.text,
    due_date: column.text,
    completed: column.integer,
  }),
})
const db = new PowerSyncDatabase({
  database: { dbFilename: 'app.sqlite' },
  schema: APP_SCHEMA,
})

const taskSchema = z.object({
  id: z.string(),
  title: z.string().nullable(),
  due_date: z
    .string()
    .nullable()
    .transform((val) => (val ? new Date(val) : null)),
  completed: z
    .number()
    .nullable()
    .transform((val) => (val != null ? val > 0 : null)),
})

const tasksCollection = createCollection(
  powerSyncCollectionOptions({
    database: db,
    table: APP_SCHEMA.props.tasks,
    schema: taskSchema,
    onDeserializationError: (error) => console.error('Fatal:', error),
    syncBatchSize: 500,
  }),
)
Referenced from SKILL.md