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-querynpm install @tanstack/react-db @tanstack/query-db-collection @tanstack/react-querypnpm add @tanstack/react-db @tanstack/query-db-collection @tanstack/react-queryValidation Library
Install your chosen validator (Zod is the default). See Validator Libraries for details.
bun add zodbun add valibotbun add arktypebun add effectGraphQL Sources
bun add graphql-requestnpm install graphql-requestpnpm add graphql-requestOpenAPI Sources
bun add @better-fetch/fetchnpm install @better-fetch/fetchpnpm add @better-fetch/fetchSetup
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 generateGenerated 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 optionsWhat Gets Generated
When "db" is specified in the generates array:
functions.ts- Standalone fetch functions for each operation (auto-generated)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 QueryqueryFn- Fetches the list data using the generated functionqueryClient- TanStack Query client for cache managementgetKey- Extracts the unique key from each itemonInsert- Persistence handler for insert mutationsonUpdate- Persistence handler for update mutationsonDelete- 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
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for this source (lowercase alphanumeric with hyphens, starting with a letter) |
type | "graphql" | Yes | Source type |
schema | object | Yes | Schema configuration (see below) |
documents | string | string[] | Yes | Glob pattern(s) for .graphql operation files |
generates | array | Yes | Must include "db" (automatically enables "query") |
overrides | object | No | Override scalars, DB, and form settings (see Configuration Reference) |
Schema Configuration
URL-based (introspection):
| Option | Type | Required | Description |
|---|---|---|---|
schema.url | string | Yes | GraphQL endpoint URL for introspection |
schema.headers | Record<string, string> | No | Headers to send with introspection request |
File-based (local SDL files):
| Option | Type | Required | Description |
|---|---|---|---|
schema.file | string | string[] | Yes | Path or glob pattern(s) for .graphql schema files |
OpenAPI Source Options
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for this source (lowercase alphanumeric with hyphens, starting with a letter) |
type | "openapi" | Yes | Source type |
spec | string | Yes | Path to OpenAPI spec (local file or URL) |
headers | Record<string, string> | No | Headers for fetching remote spec |
include | string[] | No | Glob patterns for paths to include |
exclude | string[] | No | Glob patterns for paths to exclude |
generates | array | Yes | Must include "db" (automatically enables "query") |
overrides | object | No | Override 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, orSchema.for Effect. See the Configuration Reference for examples with other validators.
Default Scalar Mappings
| GraphQL Scalar | TypeScript Type |
|---|---|
ID | string |
String | string |
Int | number |
Float | number |
Boolean | boolean |
DateTime | string |
Date | string |
JSON | unknown |
BigInt | bigint |
UUID | string |
Combining Generators
You can combine "db" with other generators:
generates: ["db", "form"]This generates:
functions.ts- Standalone fetch functionsquery/options.ts- Query and mutation optionsdb/collections.ts- TanStack DB collection optionsform/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
| Option | Type | Default | Description |
|---|---|---|---|
keyField | string | Auto-detected "id" | Field to use as unique key for items |
syncMode | "full" | "on-demand" | "full" | Data synchronization strategy |
predicateMapping | string | Auto-detected | Predicate translation preset for on-demand mode |
selectorPath | string | Auto-detected | Dot-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/dbnpm install @tanstack/dbpnpm add @tanstack/dbGenerated 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:
| Operator | REST Simple | JSON:API | Hasura | Prisma |
|---|---|---|---|---|
eq | field=value | filter[field]=value | { _eq: value } | { equals: value } |
lt | field_lt=value | filter[field][lt]=value | { _lt: value } | { lt: value } |
lte | field_lte=value | filter[field][lte]=value | { _lte: value } | { lte: value } |
gt | field_gt=value | filter[field][gt]=value | { _gt: value } | { gt: value } |
gte | field_gte=value | filter[field][gte]=value | { _gte: value } | { gte: value } |
in | field_in=value | filter[field][in]=value | { _in: value } | { in: value } |
Unsupported operators are silently ignored.
