Tangramsangrams

TanStack DB

Generate collections for TanStack DB with local-first data synchronization.

TanStack DB

Tangrams generates TanStack DB collection options that provide local-first data synchronization with optimistic updates. Collections are generated from your GraphQL queries and mutations or OpenAPI operations.

Peer Dependencies

Core

bun add @tanstack/react-db @tanstack/query-db-collection @tanstack/react-query
npm install @tanstack/react-db @tanstack/query-db-collection @tanstack/react-query
pnpm add @tanstack/react-db @tanstack/query-db-collection @tanstack/react-query

Validation Library

Install your chosen validator (Zod is the default). See Validator Libraries for details.

bun add zod
bun add valibot
bun add arktype
bun add effect

GraphQL Sources

bun add graphql-request
npm install graphql-request
pnpm add graphql-request

OpenAPI Sources

bun add @better-fetch/fetch
npm install @better-fetch/fetch
pnpm add @better-fetch/fetch

Setup

GraphQL Source

Configure a GraphQL source with "db" in the generates array:

import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "graphql",
      type: "graphql",
      schema: {
        url: "http://localhost:4000/graphql",
      },
      documents: "./src/graphql/**/*.graphql",
      generates: ["db"], // Also enables "query" automatically
    },
  ],
})

Then create your GraphQL operations in .graphql files:

# src/graphql/user.graphql

query ListUsers {
  users {
    id
    name
    email
  }
}

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
  }
}

mutation DeleteUser($id: ID!) {
  deleteUser(id: $id)
}

OpenAPI Source

Configure an OpenAPI source with "db" in the generates array:

import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "api",
      type: "openapi",
      spec: "./openapi.yaml",
      generates: ["db"], // Also enables "query" automatically
    },
  ],
})

Generate

Run the generator:

bunx tangrams generate

Generated Output

Output Directory Structure

tangrams/
└── <source>/
    ├── client.ts             # API client (shared)
    ├── schema.ts             # Validation schemas + TypeScript types
    ├── functions.ts          # Standalone fetch functions (auto-generated)
    ├── query/
    │   └── options.ts        # queryOptions and mutationOptions
    └── db/
        └── collections.ts    # TanStack DB collection options

What Gets Generated

When "db" is specified in the generates array:

  1. functions.ts - Standalone fetch functions for each operation (auto-generated)
  2. db/collections.ts - TanStack DB collection options that wire up the fetch functions

Example functions.ts:

import { getClient } from "./client"
import type { ListUsersQuery, CreateUserMutation, CreateUserMutationVariables } from "./schema"

export const listUsers = async () =>
  (await getClient()).request<ListUsersQuery>(ListUsersDocument)

export const createUser = async (variables: CreateUserMutationVariables) =>
  (await getClient()).request<CreateUserMutation>(CreateUserDocument, variables)

export const updateUser = async (variables: UpdateUserMutationVariables) =>
  (await getClient()).request<UpdateUserMutation>(UpdateUserDocument, variables)

export const deleteUser = async (variables: DeleteUserMutationVariables) =>
  (await getClient()).request<DeleteUserMutation>(DeleteUserDocument, variables)

Example db/collections.ts:

import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { createCollection } from "@tanstack/react-db"

import type { QueryClient } from "@tanstack/react-query"
import type { User } from "../schema"
import { listUsers, createUser, updateUser, deleteUser } from "../functions"

/**
 * Collection options for User
 */
export const userCollectionOptions = (queryClient: QueryClient) =>
  createCollection(
    queryCollectionOptions({
      queryKey: ["graphql", "ListUsers"],
      queryFn: async () => listUsers(),
      queryClient,
      getKey: (item) => item.id,
      onInsert: async ({ transaction }) => {
        await Promise.all(transaction.mutations.map((m) => createUser({ input: m.modified })))
      },
      onUpdate: async ({ transaction }) => {
        await Promise.all(transaction.mutations.map((m) => updateUser({ id: m.original.id, input: m.changes })))
      },
      onDelete: async ({ transaction }) => {
        await Promise.all(transaction.mutations.map((m) => deleteUser({ id: m.key })))
      },
    })
  )

Collection Options Factory

Each generated collection is a factory function that takes a QueryClient and returns a collection. This allows the collection to integrate with TanStack Query for data fetching and caching.

The generated collection options include:

  • queryKey - Cache key for TanStack Query
  • queryFn - Fetches the list data using the generated function
  • queryClient - TanStack Query client for cache management
  • getKey - Extracts the unique key from each item
  • onInsert - Persistence handler for insert mutations
  • onUpdate - Persistence handler for update mutations
  • onDelete - Persistence handler for delete mutations

Usage

Creating a Collection

Pass your QueryClient to the collection options factory:

import { QueryClient } from "@tanstack/react-query"
import { userCollectionOptions } from "./tangrams/my-api/db/collections"

const queryClient = new QueryClient()
const usersCollection = userCollectionOptions(queryClient)

Using with React

Use the collection with TanStack DB's React hooks:

import { useLiveQuery } from "@tanstack/react-db"
import { userCollectionOptions } from "./tangrams/my-api/db/collections"

function UserList({ queryClient }: { queryClient: QueryClient }) {
  const usersCollection = userCollectionOptions(queryClient)
  const { data: users } = useLiveQuery((q) => q.from({ user: usersCollection }))

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Configuration Reference

GraphQL Source Options

OptionTypeRequiredDescription
namestringYesUnique name for this source (lowercase alphanumeric with hyphens, starting with a letter)
type"graphql"YesSource type
schemaobjectYesSchema configuration (see below)
documentsstring | string[]YesGlob pattern(s) for .graphql operation files
generatesarrayYesMust include "db" (automatically enables "query")
overridesobjectNoOverride scalars, DB, and form settings (see Configuration Reference)

Schema Configuration

URL-based (introspection):

OptionTypeRequiredDescription
schema.urlstringYesGraphQL endpoint URL for introspection
schema.headersRecord<string, string>NoHeaders to send with introspection request

File-based (local SDL files):

OptionTypeRequiredDescription
schema.filestring | string[]YesPath or glob pattern(s) for .graphql schema files

OpenAPI Source Options

OptionTypeRequiredDescription
namestringYesUnique name for this source (lowercase alphanumeric with hyphens, starting with a letter)
type"openapi"YesSource type
specstringYesPath to OpenAPI spec (local file or URL)
headersRecord<string, string>NoHeaders for fetching remote spec
includestring[]NoGlob patterns for paths to include
excludestring[]NoGlob patterns for paths to exclude
generatesarrayYesMust include "db" (automatically enables "query")
overridesobjectNoOverride DB and form settings (see Configuration Reference)

Custom Scalars (GraphQL)

Map GraphQL scalars to validator expressions using overrides.scalars. Values must be valid expressions for your configured validator library:

{
  name: "my-api",
  type: "graphql",
  schema: { url: "..." },
  documents: "...",
  generates: ["db"],
  overrides: {
    scalars: {
      DateTime: "z.string()",      // Use z.coerce.date() for Date objects
      JSON: "z.unknown()",
    },
  },
}

Note: Scalar values must start with a valid prefix for your validator: z. for Zod, v. for Valibot, type( / type. for ArkType, or Schema. for Effect. See the Configuration Reference for examples with other validators.

Default Scalar Mappings

GraphQL ScalarTypeScript Type
IDstring
Stringstring
Intnumber
Floatnumber
Booleanboolean
DateTimestring
Datestring
JSONunknown
BigIntbigint
UUIDstring

Combining Generators

You can combine "db" with other generators:

generates: ["db", "form"]

This generates:

  • functions.ts - Standalone fetch functions
  • query/options.ts - Query and mutation options
  • db/collections.ts - TanStack DB collection options
  • form/options.ts - Form options with validation

Note: "db" automatically enables "query", so you don't need to specify both.

On-Demand Sync Mode

By default, collections use full sync mode where all data is fetched from the server and filtering happens client-side. For large datasets or APIs that support server-side filtering, you can enable on-demand sync mode to push predicates to the server.

Configuring On-Demand Mode

Enable on-demand sync for specific collections using the overrides.db.collections config:

import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "api",
      type: "openapi",
      spec: "./openapi.yaml",
      generates: ["db"],
      overrides: {
        db: {
          collections: {
            Product: {
              syncMode: "on-demand",
              predicateMapping: "rest-simple", // Optional: auto-detected by default
              selectorPath: "data.products",   // Optional: path to extract array from response
            },
          },
        },
      },
    },
  ],
})

Collection Override Options

OptionTypeDefaultDescription
keyFieldstringAuto-detected "id"Field to use as unique key for items
syncMode"full" | "on-demand""full"Data synchronization strategy
predicateMappingstringAuto-detectedPredicate translation preset for on-demand mode
selectorPathstringAuto-detectedDot-notation path to extract array from wrapped responses (e.g., "data.items")

Predicate Mapping Presets

Tangrams supports several predicate mapping presets that translate TanStack DB predicates to API-specific formats:

REST Simple (OpenAPI default)

Maps predicates to URL query parameters like field=value, field_lt=value, sort=field:direction:

predicateMapping: "rest-simple"

Generated translator output:

// Input: { field: "price", operator: "lt", value: 100 }
// Output: { price_lt: 100 }

// Input: { field: "category", operator: "eq", value: "electronics" }
// Output: { category: "electronics" }

JSON:API (OpenAPI)

Maps predicates to JSON:API style parameters like filter[field], filter[field][op], sort=-field:

predicateMapping: "jsonapi"

Generated translator output:

// Input: { field: "price", operator: "lt", value: 100 }
// Output: { "filter[price][lt]": 100 }

// Input: { field: "category", operator: "eq", value: "electronics" }
// Output: { "filter[category]": "electronics" }

Hasura (GraphQL default)

Maps predicates to Hasura-style GraphQL variables with where: { field: { _eq: value } }:

predicateMapping: "hasura"

Generated translator output:

// Input: { field: "price", operator: "lt", value: 100 }
// Output: { where: { price: { _lt: 100 } } }

// Input: { field: "category", operator: "eq", value: "electronics" }
// Output: { where: { category: { _eq: "electronics" } } }

Prisma (GraphQL)

Maps predicates to Prisma-style GraphQL variables with where: { field: { equals: value } }:

predicateMapping: "prisma"

Generated translator output:

// Input: { field: "price", operator: "lt", value: 100 }
// Output: { where: { price: { lt: 100 } } }

// Input: { field: "category", operator: "eq", value: "electronics" }
// Output: { where: { category: { equals: "electronics" } } }

Auto-Detection

Tangrams automatically detects the appropriate predicate mapping preset by analyzing your API schema:

  • OpenAPI: Analyzes query parameter patterns (e.g., field_lt, filter[field])
  • GraphQL: Analyzes input types for Hasura-style (*_bool_exp) or Prisma-style (*WhereInput) patterns

You only need to specify predicateMapping if you want to override the auto-detected preset.

Additional Peer Dependency for On-Demand Mode

When using on-demand sync mode, you also need @tanstack/db for the LoadSubsetOptions type:

bun add @tanstack/db
npm install @tanstack/db
pnpm add @tanstack/db

Generated On-Demand Collection

When on-demand mode is enabled, the generated collection includes a predicate translator function:

import { parseLoadSubsetOptions } from "@tanstack/query-db-collection"
import type { LoadSubsetOptions } from "@tanstack/db"

function translateProductPredicates(
  options?: LoadSubsetOptions
): Partial<ListProductsParams> {
  if (!options) return {}

  const parsed = parseLoadSubsetOptions(options)
  const params: Record<string, unknown> = {}

  for (const filter of parsed.filters) {
    const fieldName = filter.field.join(".")
    switch (filter.operator) {
      case "eq":
        params[fieldName] = filter.value
        break
      case "lt":
        params[`${fieldName}_lt`] = filter.value
        break
      // ... other operators
    }
  }

  // Handle sorting and pagination
  if (parsed.sorts.length > 0) {
    params["sort"] = parsed.sorts
      .map((s) => `${s.direction === "desc" ? "-" : ""}${s.field.join(".")}`)
      .join(",")
  }

  if (parsed.limit != null) params["limit"] = parsed.limit
  if (parsed.offset != null) params["offset"] = parsed.offset

  return params as Partial<ListProductsParams>
}

export const productCollectionOptions = (queryClient: QueryClient) =>
  createCollection(
    queryCollectionOptions({
      queryKey: ["Product"],
      syncMode: "on-demand",
      queryFn: async (ctx) => {
        const params = translateProductPredicates(ctx.meta?.loadSubsetOptions)
        return listProducts(params)
      },
      queryClient,
      getKey: (item) => item.id,
      // ... mutation handlers
    })
  )

Supported Operators

The predicate translator supports these TanStack DB operators:

OperatorREST SimpleJSON:APIHasuraPrisma
eqfield=valuefilter[field]=value{ _eq: value }{ equals: value }
ltfield_lt=valuefilter[field][lt]=value{ _lt: value }{ lt: value }
ltefield_lte=valuefilter[field][lte]=value{ _lte: value }{ lte: value }
gtfield_gt=valuefilter[field][gt]=value{ _gt: value }{ gt: value }
gtefield_gte=valuefilter[field][gte]=value{ _gte: value }{ gte: value }
infield_in=valuefilter[field][in]=value{ _in: value }{ in: value }

Unsupported operators are silently ignored.

On this page