Nocly.fr
Back to articles
6 min read

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.

Next.jsi18nSupabaseUX

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.