Store a Date-Only Value in a Database with Temporal
To store a date with no time or zone, use a DATE column and Temporal.PlainDate.toString() so the value can never shift across timezones. Part of Serializing and Storing Temporal Values.
Why this scenario is tricky
A date-only value — a birthday, an invoice date, a holiday — has no time and no zone. Store it in a timestamptz column and the database attaches midnight in some zone; read it back in another zone and it can shift to the previous day. The whole class of off-by-one bugs comes from letting a zone touch a value that should never have had one.
Minimal working solution
import { Temporal } from '@js-temporal/polyfill';
const dob = Temporal.PlainDate.from('1990-02-14');
const value = dob.toString(); // '1990-02-14' → store in a DATE column
const back = Temporal.PlainDate.from(value); // exact same civil date
Full production version
Guard the boundary so a Date or ZonedDateTime can never sneak into the date-only field.
import { Temporal } from '@js-temporal/polyfill';
function toDateColumn(v: Temporal.PlainDate | string): string {
const d = typeof v === 'string' ? Temporal.PlainDate.from(v) : v;
return d.toString(); // always 'YYYY-MM-DD', never a time or zone
}
Verification snippet
Common pitfalls
Frequently Asked Questions
Why does a stored birthday show the wrong day for some users?
It was stored in a timestamp-with-zone column, which attaches midnight in one zone; read in another zone that midnight moves to the previous or next day. Store date-only values in a DATE column using Temporal.PlainDate so no zone is ever applied.
Should I use timestamptz for an invoice date?
No. An invoice date is a civil date with no time, so timestamptz only adds ambiguity. A DATE column holding PlainDate.toString() keeps it exact and identical for every reader regardless of their zone.