Handling time zones in a booking system
How to display slots in the visitor's time zone without breaking consistency for the admin — a look at the Nocly.fr booking architecture.
The problem
When a visitor in Montréal books a slot showing 2:00 PM and gets an email confirming a meeting at 8:00 PM, they bail. Multiply that by four locales — fr-FR, fr-CA, en-US, en-CA — and every slot becomes a headache. The rule is simple: the server stores in UTC, the client renders in the visitor's time zone, and every email explicitly states which time zone was used.
Client side: Intl.DateTimeFormat
Instead of pulling in a library like moment-timezone (180 KB gzip), I use the native Intl.DateTimeFormat API. It detects the user's time zone and formats the date accordingly — zero dependencies.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// e.g. "Europe/Paris" or "America/Toronto"
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: "full",
timeStyle: "short",
timeZone: tz,
}).format(new Date(slot.starts_at));Server side: store the visitor's time zone
When the booking is created, the client sends its IANA time zone in the payload. I store it in the timezone column of the bookings table. This lets me regenerate emails and the ICS file in the correct zone, even if the visitor returns from a different device.
Gotchas to avoid
First gotcha: never compare local times — always convert to UTC first. Second: an ICS file must declare the time zone in DTSTART;TZID=…, otherwise Outlook and Google Calendar interpret it differently. Third: daylight saving transitions can break a recurring slot — prefer one-off slots stored in UTC over a local rule.
This post is part of a series on the technical choices behind Nocly.fr. Questions? The contact form is open.