QUESTPIE
SchemaFields

Datetime field

`f.datetime()` stores one instant. The column is a Postgres `timestamp`, every read hands back a JS `Date`, and the only string it accepts carries a zone.

View markdown
CallColumnValue
f.datetime()timestamp(3) with time zoneDate
f.datetime({ precision: n })timestamp(n) with time zoneDate
f.datetime({ withTimezone: false })timestamp(3)Date

Both keys are constructor-only. No method changes them later.

src/questpie/server/collections/events.ts
import { collection } from "#questpie/factories";

export const events = collection("events").fields(({ f }) => ({
	startsAt: f.datetime().required(), // timestamptz(3), NOT NULL
	publishedAt: f.datetime(), // nullable
	tickAt: f.datetime({ precision: 6 }), // microseconds
	wallClock: f.datetime({ withTimezone: false }), // no zone
}));

The field selects as Date | null. Add .required() and it selects as Date.

Options

OptionTypeDefaultEffect
precision0 to 63Fractional-second digits in the column
withTimezonebooleantruetrue gives timestamptz, false gives bare timestamp

What it accepts

The derived schema is a union of z.date() and z.iso.datetime({ offset: true }). The string branch converts to a Date before the write.

InputResult
new Date(...)Accepted
"2026-01-01T00:00:00.000Z"Accepted, becomes a Date
"2026-01-01T00:00:00+02:00"Accepted, becomes a Date
"2026-01-01T00:00:00"Rejected, no zone
"2026-01-01"Rejected, a calendar day is not an instant
1767225600000Rejected, epoch numbers are not parsed
nullStored, unless .required()

No string appears in the generated insert type. The string branch is there for a plain JSON body.

Reads run the other way. A stored value comes back as a Date. A null passes through untouched. A value that parses as neither throws a TypeError before it reaches your code.

Methods

The type adds two methods on top of the base chain.

MethodEffect
.autoNow()Sets hasDefault and a () => new Date() factory
.autoNowUpdate()Adds a beforeChange field hook returning new Date()

Neither method changes the field's type. So f.datetime().required().autoNow() still demands the key in TypeScript. The runtime schema accepts it missing. Both methods exist on f.date() too. There they produce a YYYY-MM-DD string.

`.autoNow()` freezes one instant into the column

The factory runs once, when the column is built. Its result becomes a literal column DEFAULT. A row inserted without the field gets the moment the DDL was generated, not the moment of the insert. .default(() => …) behaves the same way.

`.autoNowUpdate()` fires only on keys you send

Field hooks run over the keys present in the payload. Omit the field and the hook never runs. .inputFalse() drops the field from both generated schemas. So the two together never fire at all.

Leave the collection's timestamps option on for a created and updated pair that works. It adds created_at and updated_at as timestamp(3) columns without a zone. Both default to now(). The CRUD layer rewrites updated_at on every update. See Options.

Filtering

The datetime field carries the dateOps set, eleven operators. Comparison operands are Date | string. The null checks take a boolean.

OperatorOperandMatches
eq / neDate | stringEqual, not equal
notDate | string, or nullNot equal, or IS NOT NULL when given null
gt / gteDate | stringAfter, at or after
lt / lteDate | stringBefore, at or before
in / notIn(Date | string)[]In the list, not in the list
isNull / isNotNullbooleanPass false and each inverts
const now = new Date();
const { docs } = await app.collections.events.find({
	where: {
		startsAt: { gte: now, lte: new Date(now.getTime() + 86_400_000) },
		publishedAt: { isNotNull: true },
	},
	orderBy: { startsAt: "asc" },
});

f.time() carries this same set. Its stored values are strings.

In the admin

The form control is DatetimeField. The list cell is DateTimeCell.

  • On desktop the control is a popover holding a calendar plus a time input. On mobile it is a native datetime-local input.
  • The control's own precision prop is "minute" or "second". It defaults to "minute". It has nothing to do with the column's numeric precision. Nothing carries one to the other.
  • .admin() on a datetime field takes the date field's meta. That meta adds only placeholder to the shared keys. The control's minDate, maxDate and format props are not in it.
  • The list cell prints the date and drops the time. It drops the year too when it matches the current one.

Temporal values covers the wire, the typed client, realtime and SSR. An instant has to survive a JSON hop in each one.

f.date() is the calendar day. It stores an exact YYYY-MM-DD string in a date column. That string never becomes a Date.

f.time() is the time of day. It stores a string in a time column.

Options covers timestamps and the rest of the per-collection switches.

Arrays covers .array(). It swaps the column for jsonb and the operators for the multi-value set.

Validation covers the derived schema and .zod(). Use .zod() for a rule the constructor has no key for.

On this page