Why Agnostic Query?
The Problem
Section titled “The Problem”You use TanStack DB to build queries on the client, and Drizzle to execute them on the server. But they speak different query languages — the client produces LoadSubsetOptions, the server expects Drizzle conditions, and there’s no serialisable format to pass the query across the wire.
The result:
- Duplicate code: the same filtering logic written twice
- No shared type safety: a change to one side doesn’t flag mismatches on the other
- Manual JSON bridging: you invent your own query-object format and write ad-hoc converters
- No validation: no way to guarantee the client isn’t sending a malformed query
Before & After
Section titled “Before & After”Here’s the same scenario — a client query driven by TanStack DB, executed by Drizzle on the server — built without and with agnostic-query.
Without agnostic-query
Section titled “Without agnostic-query”The official TanStack DB docs show this pattern (Quick Start: Simple REST API):
// Client: manually parse LoadSubsetOptions and build URL paramsimport { parseLoadSubsetOptions } from '@tanstack/db'
queryFn: async (ctx) => { const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions) const params = new URLSearchParams()
parsed.filters.forEach(({ field, operator, value }) => { const fieldName = field.join('.') if (operator === '=') params.set(fieldName, String(value)) if (operator === '<') params.set(`${fieldName}_lt`, String(value)) // ... handle every operator manually })
parsed.sorts.forEach(s => { params.set('sort', `${s.field.join('.')}:${s.direction}`) })
if (parsed.limit) params.set('limit', String(parsed.limit))
const response = await fetch(`/api/products?${params}`) return response.json()}// Server: receive URL params, manually translate to Drizzle// No shared type, no validationexport const listProject = createServerFn() .handler(async ({ data }) => { const conditions = [] if (data.name) conditions.push(eq(projectTable.name, data.name)) if (data.age_lt) conditions.push(lt(projectTable.age, Number(data.age_lt))) // ... each filter mapped manually
const rows = await db.select() .from(projectTable) .where(and(...conditions)) .orderBy(...) .limit(data.limit) return rows })With agnostic-query
Section titled “With agnostic-query”One call on each side, shared types, automatic validation:
// Client: fromTanDb handles where, cursor, limit, orderBy in one lineimport { fromTanDb } from 'agnostic-query/tanstack-db'
const data = fromTanDb(meta?.loadSubsetOptions)// → typed QuerySchema<Project>// Server: receive QuerySchema, execute via Drizzle, validate automaticallyimport { toDrizzle } from 'agnostic-query/drizzle/pg'import { createQuerySchema } from 'agnostic-query/zod'
export const listProject = createServerFn() .inputValidator(createQuerySchema<Project>()) .handler(async ({ data }) => { return await toDrizzle(db, projectTable, data) // → typed Promise<Project[]> })The Solution
Section titled “The Solution”agnostic-query defines a portable QuerySchema — a plain JSON format that any of these libraries can convert to and from:
TanStack DB ──fromTanDbWhere──> QuerySchema ──toDrizzle──> Drizzleaq builder ──.toJSON()──────> QuerySchema ──toKysely──> KyselyKysely query ──fromKysely─────> QuerySchema ──toSql──────> Raw SQLClient → Server flow
Section titled “Client → Server flow”- Client: TanStack DB collection’s
queryFnreceives its internal WHERE/ORDER BY expressions - Translation:
fromTanDbconverts them intoQuerySchemain one call - Serialization: the
QuerySchemais sent as JSON to the server (HTTP, RPC, server fn) - Execution: the server validates it with Zod/Valibot and executes via
toDrizzle
What It Is Not
Section titled “What It Is Not”agnostic-query is not an ORM. It doesn’t manage connections, schemas, or migrations. What it does provide are tree-shakeable adapters (toDrizzle, toKysely, toDb0) that convert or execute queries on the server — so you can send a portable QuerySchema from the client and run it against any backend without coupling your client code to a specific ORM.
When Should You Use It?
Section titled “When Should You Use It?”- You use TanStack DB on the client and Drizzle or Kysely on the server
- You want to share query logic between browser and server without duplication
- You want type safety across the wire — the server validates that the incoming query matches your schema
- You want to switch databases or ORMs without rewriting client code