QUESTPIE
Build Your BackendArchitecture

Single Source of Truth

Fields drive everything — database, validation, query operators, types, and UI rendering from one definition.

In QUESTPIE, a field definition is not just a form widget or a database column. It's the single source of truth that drives every layer of the stack.

What One Field Definition Produces

price: f.number().required().label("Price (cents)");

From this single line, QUESTPIE derives:

LayerOutput
Databaseinteger NOT NULL column in Drizzle schema
API validationz.number() in the create/update Zod schema
Query operatorswhere: { price: { gte: 1000, lt: 5000 } }
TypeScript typesprice: number in Select and Insert types
Client SDKTyped find(), create(), update() with price field
Admin listNumeric column with sorting
Admin formNumber input with "Price (cents)" label
Admin filtersNumeric range filter

No Dual Definitions

Traditional approaches require you to define schema in multiple places:

// Without QUESTPIE — same data defined 3+ times
Database migration   → INTEGER price NOT NULL
API validation       → z.object({ price: z.number() })
TypeScript type      → interface Post { price: number }
Form component       → <NumberInput name="price" label="Price" required />

With QUESTPIE:

// With QUESTPIE — defined once
f.number().required().label("Price")
→ database, validation, types, UI all derived automatically

How It Works

  1. You define fields in a collection file
  2. Codegen reads your field definitions
  3. Generated types flow to the runtime, client, and admin
  4. Each consumer uses the appropriate projection of the same definition
Field Definition
  ├──→ Drizzle column type + constraints
  ├──→ Zod validation schema
  ├──→ TypeScript Select/Insert types
  ├──→ Query operator type narrowing
  └──→ Admin field renderer selection (via registry)

Practical Consequences

Add a field → everything updates

Add a field to a collection, re-run codegen:

// Before
.fields(({ f }) => ({
  title: f.text().required(),
  body: f.textarea(),
}))

// After — add status field
.fields(({ f }) => ({
  title: f.text().required(),
  body: f.textarea(),
  status: f.select(["draft", "published"]).default("draft"),
}))

After questpie generate:

  • Database gets a new status column
  • API validates status on create/update
  • Client SDK exposes where: { status: "draft" }
  • Admin form shows a select dropdown
  • Admin list gets a new filterable column

Field type → query operators

The field type determines which query operators are available:

// text → equals, contains, startsWith, endsWith
where: { title: { contains: "hello" } }

// number → equals, gt, gte, lt, lte, in
where: { price: { gte: 1000, lt: 5000 } }

// datetime → equals, gt, gte, lt, lte
where: { scheduledAt: { gte: startOfDay, lte: endOfDay } }

// select → equals, in
where: { status: "published" }
where: { status: { in: ["draft", "published"] } }

// boolean → equals
where: { isActive: true }

// relation → equals (by ID)
where: { barber: barberId }

Field type → admin renderer

The field type maps to an admin form component:

Field TypeDefault Renderer
textText input
textareaTextarea
richTextRich text editor (TipTap)
numberNumber input
booleanCheckbox / switch
dateDate picker
datetimeDate-time picker
selectSelect dropdown
relationRelation picker
uploadFile upload with preview
objectNested form group
arrayRepeatable items
blocksBlock editor
  • Fields — All field types and options
  • Codegen — What gets generated
  • Querying — Query operators per field type

On this page