Dates, timezones, UTC, and ISO 8601. Every timestamp we send and accept: 2026-08-07T22:15:30.123+...Dates, timezones, UTC, and ISO 8601. Every timestamp we send and accept: 2026-08-07T22:15:30.123+...
The network for creativity
Join 1.25M professional creatives like you
Connect with clients, get discovered, and run your business 100% commission-free
Creatives on Contra have earned over $150M and we are just getting started
Dates, timezones, UTC, and ISO 8601.
Every timestamp we send and accept:
2026-08-07T22:15:30.123+03:00
2026-08-07 → the date, YYYY-MM-DD. Year, month, day. Largest unit first, so sorting these strings alphabetically sorts them by time.
T → separator between date and time.
22:15:30 → hours, minutes, seconds.
.123 → milliseconds. Three digits is all a JS Date holds. Postgres goes to six, microseconds.
+03:00 → offset from UTC, so this came from somewhere on UTC+3.
+00:00 → UTC itself. Z is shorthand for the same thing.
nothing at the end → no zone information.
UTC is not GMT. UTC is a standard fixed at zero. GMT is a timezone the UK drops for BST in summer.
Not every date is an instant.
A birthday is a calendar date. Someone born 1990-03-15 was born on the 15th everywhere. Store it as date, no zone, never convert it.
Opening hours are a time of day. 09:00 means 09:00 wherever the door is. That's a time column.
Everything else is an instant. Instants are timestamptz.
The zone belongs to the record, not to whoever is looking.
An event stored at 15:00 in Africa/Cairo shows as 13:00 in London, correctly. But if the zone came from the viewer, saving a typo fix reads it back as 15:00 London and moves the event to 17:00 Cairo.
Store the IANA name, not the offset.
Africa/Cairo, America/New_York. They come from the IANA time zone database.
An offset is only correct for one instant. Egypt dropped DST in 2015 and restored it in 2023, so 14:00 in Cairo in 2020 and 2024 are different offsets. The name knows that, +02:00 doesn't.
Day numbers don't agree.
JS getDay() is 0=Sunday to 6=Saturday.
ISO 8601 is 1=Monday to 7=Sunday, what Postgres EXTRACT(ISODOW) returns.
Postgres EXTRACT(DOW) is 0=Sunday, matching JS but not ISO.
Pick one and convert at the edge: iso = ((jsDay + 6) % 7) + 1, back is iso % 7.
Adding a day to a date string: anchor at noon, not midnight.
const anchor = new Date(${day}T12:00:00Z); return formatInTimeZone(addDays(anchor, 1), timeZone, 'yyyy-MM-dd');
At midnight, one hour of DST puts you on the wrong date. Noon is twelve hours from either edge.
Let Postgres do the timezone work.
now() AT TIME ZONE COALESCE(time_zone, 'UTC') gives local time per row, without pulling rows into app code to compare.
Overlap checks stay half-open, end computed in SQL:
appointment_date_time < (:start::timestamptz + (:duration * INTERVAL '1 minute')) AND appointment_date_time + (duration_minutes * INTERVAL '1 minute') > :start::timestamptz
Half-open means 10:00-11:00 and 11:00-12:00 don't conflict. And INTERVAL arithmetic on timestamptz survives a DST night, which start + 3600000 in JS does not.
hashtag#SoftwareEngineering hashtag#Backend hashtag#PostgreSQL hashtag#NodeJS hashtag#TypeScript hashtag#ISO8601 hashtag#Timezones hashtag#DateTime hashtag#DatabaseDesign
Post image
Back to feed
The network for creativity
Join 1.25M professional creatives like you
Connect with clients, get discovered, and run your business 100% commission-free
Creatives on Contra have earned over $150M and we are just getting started