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.
| Call | Column | Value |
|---|---|---|
f.datetime() | timestamp(3) with time zone | Date |
f.datetime({ precision: n }) | timestamp(n) with time zone | Date |
f.datetime({ withTimezone: false }) | timestamp(3) | Date |
Both keys are constructor-only. No method changes them later.
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
| Option | Type | Default | Effect |
|---|---|---|---|
precision | 0 to 6 | 3 | Fractional-second digits in the column |
withTimezone | boolean | true | true 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.
| Input | Result |
|---|---|
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 |
1767225600000 | Rejected, epoch numbers are not parsed |
null | Stored, 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.
| Method | Effect |
|---|---|
.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.
| Operator | Operand | Matches |
|---|---|---|
eq / ne | Date | string | Equal, not equal |
not | Date | string, or null | Not equal, or IS NOT NULL when given null |
gt / gte | Date | string | After, at or after |
lt / lte | Date | string | Before, at or before |
in / notIn | (Date | string)[] | In the list, not in the list |
isNull / isNotNull | boolean | Pass 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-localinput. - The control's own
precisionprop is"minute"or"second". It defaults to"minute". It has nothing to do with the column's numericprecision. Nothing carries one to the other. .admin()on a datetime field takes the date field's meta. That meta adds onlyplaceholderto the shared keys. The control'sminDate,maxDateandformatprops 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.
Related
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.