How Should a SaaS App Handle Time Zones?
How Should a SaaS App Handle Time Zones?
Store the instant, display the context. Save every timestamp as an unambiguous moment in UTC, keep the user's IANA time zone as a separate stored preference, and render dates in that zone with the zone visible on screen. Most time zone bugs come from skipping one of those three steps.
This sounds obvious and it is still the most common source of quiet, embarrassing bugs we find in product work. A scheduled report that arrives a day early. An audit log that disagrees with a support ticket. A deadline that passes an hour before the customer expected.
None of these are hard problems. They are decisions nobody made, so the framework picked a default.
Why Is the Browser's Time Zone Not Enough?
Because it describes the device, not the person. JavaScript gives you the browser's guess through Intl.DateTimeFormat, where resolvedOptions().timeZone returns the user's default time zone as an IANA identifier such as America/New_York or Australia/Sydney.
That is a good default for a first visit. It is a bad single source of truth. A customer support agent travelling to a conference will have their laptop report a different zone all week, and their ticket view will silently reinterpret every timestamp. A user on a VPN may get a zone from another continent.
So we detect the browser zone, offer it, and then store the user's chosen zone on their account. Detection is a suggestion. The stored preference is the answer, which is why it belongs on the settings page where the user can see and change it.
Why Store an IANA Name Instead of an Offset?
Because offsets change and names do not. Storing "UTC plus 10" for a Sydney user is wrong for half the year. Storing "Australia/Sydney" is correct always, because the rules for that name carry the daylight saving history with them.
Those rules live in the IANA Time Zone Database, also known as tz or zoneinfo, which IANA describes as code and data representing the history of local time for many representative locations worldwide. It is updated periodically to reflect changes made by political bodies to time zone boundaries, UTC offsets and daylight saving rules.
The phrase "political bodies" is the whole reason this matters. Time zones are legislation, not physics.
How Often Do Time Zone Rules Actually Change?
Several times a year. Version 2026d of the tz database was released on 11 September 2026, and among its changes is Canada's Northwest Territories moving to a permanent offset of UTC minus 06.
If your product has customers in Yellowknife and your servers or client libraries are running an older copy of that database, their timestamps are now wrong. Not by a rounding error, by a full hour, and nothing in your code changed.
This is the part teams forget to own. The tz database is a dependency like any other, and it needs a person responsible for updating it. We treat a stale tz data version the same way we treat a stale TLS library: it is a scheduled maintenance item, not a surprise.
What Should a Timestamp Look Like on Screen?
It should never be ambiguous. A bare "3:00 PM" in a shared workspace is a guess, and every reader resolves it differently. The fix is to show the zone whenever more than one person can see the same screen.
Intl.DateTimeFormat gives you six ways to render that zone through the timeZoneName option: short, long, shortOffset, longOffset, shortGeneric and longGeneric. Short gives you a familiar abbreviation, the offset forms give you an unmistakable number, and the generic forms avoid implying daylight saving state.
Our default in shared contexts is a short zone label next to the time. In a single user context, such as a personal dashboard, the label can be dropped, because there is only one interpretation available.
When Is Relative Time the Wrong Choice?
When the exact moment matters. "3 hours ago" is friendly and it destroys information. It is right for an activity feed and wrong for an audit log, a billing event, a legal record, or anything a support agent will read back to a customer.
The pattern we use is relative on the surface and absolute on hover or tap. The list stays scannable, and the precise timestamp with its zone is one interaction away. That single habit removes most of the "the log says something different to the email" conversations.
In a data table the calculation flips. Tables are for comparing, and relative times of different ages are hard to compare. Absolute is usually the better default there.
What About Dates With No Time At All?
Treat them as dates, not as midnight. A birthday, a contract start date, or an invoice date has no time zone. Storing it as a timestamp at midnight UTC guarantees that someone in Auckland or Los Angeles eventually sees the wrong day.
This distinction is exactly what the legacy JavaScript Date object could not express. MDN lists the flaws plainly: a Date acts as both a timestamp and a set of date components, it supports only UTC and the local device zone, and it has no concept of wall clock time that is independent of daylight saving.
So use a date only column in the database and a date only type in the code. The rule is that the type should describe the thing, not the storage convenience.
Is the Temporal API Ready to Use?
Not yet as a default. Temporal is designed as a complete replacement for Date, with distinct types for different jobs: Temporal.Instant for an exact moment, Temporal.ZonedDateTime for a moment in a named zone, Temporal.PlainDate for a calendar date, Temporal.PlainTime, Temporal.Duration and more. It is not a constructor, and it exposes a large static API.
MDN currently states that the feature is not Baseline because it does not work in some of the most widely used browsers. So for production today, we design the data model the Temporal way and implement it with whatever library the stack already uses.
That is the useful move regardless of availability. Temporal's type split is a modelling lesson, and adopting the distinction between an instant, a zoned time and a plain date will fix more bugs than switching libraries ever will.
How Do Scheduled Actions Change the Calculation?
They are where the real money bugs live. A weekly report set for 9am, a trial expiring in 14 days, a reminder sent the morning of a meeting: all of these are wall clock intentions, not fixed instants.
If you store "next run at this UTC instant" and a daylight saving change happens in between, the user's 9am report arrives at 8am or 10am. Store the intention instead: the local time, the zone, and the recurrence rule. Compute the next instant each time, from current tz data.
Also decide explicitly what happens to times that do not exist or happen twice. On a spring forward morning, 2:30am may not exist. On an autumn morning, it may happen twice. Pick a rule, write it down, and make sure your notification design does not send the same alert twice because of it.
What Would We Check on an Existing Product?
Five things, in order. Are timestamps stored as unambiguous instants. Is there a stored user time zone preference separate from browser detection. Are zone names stored as IANA identifiers rather than offsets. Is the tz database on a maintenance schedule. Do shared screens show the zone.
Then one test that finds most of the rest: set your machine to a zone on the other side of the date line, use the product for ten minutes, and write down everything that looks wrong. It is a cheap exercise and it never comes back empty.
If your team is arguing about a timestamp bug that only happens for some customers, this is usually the shape of it, and we are happy to look at the model with you. You can find us at phoenix.studio.
Want a site that performs like this?
Tell us about your project. We will come back with a clear next step, no pressure.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
Have a project like this?
Tell us where you want to go. We'll tell you how we'd get you there.