# Datetime field (/docs/schema/fields/datetime)

---
title: Datetime field
description: "`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."
kind: reference
package: questpie
---

| 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.

```ts title="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

| 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.

<Callout type="warn" title="`.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.
</Callout>

<Callout type="warn" title="`.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.
</Callout>

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](/docs/schema/collections/options#timestamps).

## 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               |

```ts
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.

## Related

[Temporal values](/docs/schema/fields/temporal-values) covers the wire, the
typed client, realtime and SSR. An instant has to survive a JSON hop in each
one.

[`f.date()`](/docs/schema/fields/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()`](/docs/schema/fields/time) is the time of day. It stores a string in
a `time` column.

[Options](/docs/schema/collections/options) covers `timestamps` and the rest of
the per-collection switches.

[Arrays](/docs/schema/fields/array) covers `.array()`. It swaps the column for
`jsonb` and the operators for the multi-value set.

[Validation](/docs/schema/validation) covers the derived schema and `.zod()`.
Use `.zod()` for a rule the constructor has no key for.
