Subtitle: The three-clock model I used to build a production scheduling platform across countries, vendors, and daylight saving time — and how to apply it to your own system.
Introduction
Timezones seem simple — until your application goes live.
Every booking system works perfectly in development:
- Users select a date.
- Slots are generated.
- Bookings are stored.
- Notifications go out.
Then customers arrive from different countries.
And suddenly you’re debugging things like:
- “Today” behaves like yesterday.
- Customers can’t book same-day appointments.
- Workers vanish from the availability grid.
- Reminder emails fire an hour early.
- A DST change silently shifts every appointment by one hour.
Here’s the uncomfortable truth:
These aren’t edge cases. They’re architectural problems.
I hit every single one of these while building a multi-tenant booking platform for a European client — vendors in Australia, customers worldwide, workers in different cities. This article is the architecture I wish I had on day one.
If you’re building anything with scheduling — bookings, appointments, deliveries, shifts, reminders — this is how to structure it so it scales without a rewrite.
The Biggest Mistake Developers Make
Most applications assume there is only one “current time.”
const now = new Date();
Looks harmless.
It isn’t.
That line gives you the server’s time. Or the browser’s time. Neither of which your business logic should care about.
In reality, every scheduling system has three different clocks running at once.
The Three Clocks
1. UTC — The Storage Clock
The universal timeline.
- Every timestamp in your database should be an exact moment in UTC.
- Example:
2026-07-18T04:00:00Z
Why UTC?
- It never changes.
- No daylight saving.
- No ambiguity.
- Every other time can be derived from it.
2. Business Time — The Vendor’s Clock
Every business runs on its own local clock.
Example:
- Timezone:
Australia/Sydney - Monday: 9:00 AM – 5:00 PM
Everything related to business rules must run in this timezone:
- Working hours
- Lunch breaks
- Holidays
- Booking cutoffs
- Cancellation policies
- What “today” means
- Store opening hours
3. Customer Time — The Display Clock
Customers should always see times in their own timezone.
- Someone booking from London shouldn’t mentally convert Sydney time.
- Presentation belongs to the user.
- Business logic belongs to the vendor.
The Golden Rule
If you remember one thing from this article, make it this:
Store → UTC Business Logic → Vendor Timezone Display → Customer Timezone
Never mix these responsibilities.
Almost every timezone bug I’ve ever debugged came from violating one of these three lines.
What Should You Actually Store?
A good schema separates dates, times, and timestamps by their meaning — because they behave differently.
| Data | Store As | Example |
|---|---|---|
| Booking timestamp | UTC timestamp | 2026-07-18T04:00:00Z |
| Vendor timezone | IANA timezone name | Australia/Sydney |
| Working hours | Local time strings | 09:00, 17:00 |
| Holiday | Local calendar date | 2026-12-25 |
| Reminder sent at | UTC timestamp | 2026-07-17T22:00:00Z |
| Audit logs | UTC timestamp | — |
Notice the pattern:
- Moments in time → UTC.
- Recurring rules → local time.
- Location of the clock → IANA name.
Never Store Fixed UTC Offsets
This one breaks more production systems than anything else.
Many developers store:
UTC+10
GMT+11
This breaks the moment daylight saving kicks in.
Sydney switches between:
- Winter →
UTC+10 - Summer →
UTC+11
If you saved UTC+10, every summer booking is one hour wrong.
Always store the IANA identifier instead:
Australia/Sydney
The timezone database (built into every serious datetime library) resolves the correct offset for that specific date — including DST transitions, historical changes, and government rule updates.
You should never be computing offsets yourself.
Daylight Saving Time (DST), Handled Correctly
Here’s how the right architecture makes DST a non-event.
A customer books:
- 15 January, 9:00 AM, Sydney → January uses
UTC+11→ stored as2026-01-14T22:00:00Z - The same 9:00 AM slot in July → July uses
UTC+10→ stored as2026-07-18T23:00:00Z(previous day in UTC)
Notice what happened:
- The business still opens at 9:00 AM. Always.
- The UTC timestamp shifts automatically.
- The business rule never changes.
This is exactly why business schedules must be stored as local times, not UTC timestamps. The rule is “9 AM local” — not “10 PM UTC.”
Availability Generation: The Right Pipeline
A common mistake is generating slots in UTC and converting at the end. Business rules get evaluated in the wrong clock, and slots drift across day boundaries.
Do it in this order instead:
Load vendor timezone
↓
Load working hours (local time)
↓
Generate slots in vendor local time
↓
Convert each slot to UTC
↓
Return to client
↓
Display in customer timezone
The key principle:
Business rules are never evaluated in UTC. UTC is for storage and transport only.
“Today” Is Different for Everyone
This one catches almost everybody.
At the same instant:
- Customer in India → 11:30 PM (still “today”)
- Vendor in Sydney → 4:00 AM (“tomorrow” already)
- Server in London → 6:00 PM (irrelevant)
So which one is “today”?
It depends on the question you’re asking:
- Availability & cutoffs → the vendor’s today.
- UI labels (“Today”, “Tomorrow”) → the customer’s today.
- Storage → UTC has no concept of “today” at all.
If your code contains new Date().setHours(0,0,0,0) anywhere near booking logic — you have this bug. You just haven’t seen it yet.
Working Hours Are Rules, Not Timestamps
Store this:
{ "day": "monday", "open": "09:00", "close": "17:00" }
Not this:
2026-07-18T09:00:00Z
Working hours are recurring business rules. They repeat every week, they follow the local clock, and they survive DST changes untouched — but only if you store them as local time.
Booking Cutoffs Done Right
Naive version:
if (new Date() > cutoff) { ... } // server timezone decides
Correct approach:
Get vendor's current time (in vendor timezone)
↓
Get booking's local datetime (in vendor timezone)
↓
Compute the difference
↓
Compare against the cutoff rule
The server’s timezone should never decide whether a customer can book, cancel, or reschedule. Your Cloud Run instance in us-central1 has no business voting on a Sydney salon’s cancellation policy.
Multi-Vendor: One Timezone Per Vendor
In a multi-tenant platform, timezone is a vendor-level property, not a system-level one.
| Vendor | Timezone |
|---|---|
| Vendor A | Australia/Sydney |
| Vendor B | Australia/Perth |
| Vendor C | America/New_York |
| Vendor D | Europe/London |
Two rules:
- Business logic always executes in that vendor’s timezone.
- The customer’s location is irrelevant to scheduling rules — it only affects display.
Get this right and adding a vendor in a new country requires zero code changes.
Notifications and Reminders
- Store the reminder’s fire time in UTC — that’s what your scheduler/cron compares against.
- When generating the message content, convert:
UTC (stored)
↓
Vendor timezone → "Your 9:00 AM appointment at..."
↓
Customer timezone → "That's 6:30 PM your time"
Everyone sees the correct local time. Nobody gets a reminder an hour early after a DST switch.
Calendar Invites (ICS / Google Calendar)
- Calendar formats require exact UTC timestamps plus timezone metadata.
- Never generate invites from browser local time.
- Pass the vendor’s IANA timezone in the event — calendar apps handle the rest, including DST.
Common Mistakes — The Checklist
Audit your codebase against this list:
- Using
new Date()for business logic - Letting the server’s timezone influence decisions
- Saving UTC offsets (
UTC+10) instead of IANA names (Australia/Sydney) - Mixing browser time with vendor time
- Storing working hours as UTC timestamps
- Comparing calendar dates across different timezones
- Assuming every country observes DST (many don’t — and some regions within a country differ)
If any of these exist in your system, you have a timezone bug waiting for a DST transition to expose it.
Recommended Architecture
Browser
│
▼
Select Date
│
▼
Backend
│
▼
Load Vendor Timezone
│
▼
Generate Slots (Vendor Time)
│
▼
Convert Booking → UTC
│
▼
Store in Database
│
▼
Return Localised Times
│
▼
Display in Customer Timezone
Technology Recommendations
- Storage: UTC timestamps, always.
- Timezone identifiers: IANA names (
Australia/Sydney,Europe/London). - Business rules: evaluated in the vendor’s timezone.
- Display: customer’s timezone.
- Libraries: use Temporal (where available) or Luxon for timezone and DST math. Never build business logic on the native
Dateobject.
Your Action Plan
If you’re building (or fixing) a scheduling system, do this — in order:
- Add an IANA timezone field to every vendor/business entity.
- Convert all stored timestamps to UTC. No exceptions.
- Move working hours, holidays, and cutoffs to local-time rules.
- Refactor slot generation to run in the vendor’s timezone first, then convert.
- Replace every
new Date()in business logic with an explicit timezone-aware call. - Test one booking on both sides of a DST transition. If the local time holds steady while the UTC timestamp shifts — you’ve done it right.
FAQs: Timezone Handling in Booking Systems
Direct answers to the questions developers actually search for.
Should I store dates in UTC or local time in my database?
Store timestamps (exact moments — bookings, logs, reminders) in UTC. Store recurring rules (working hours, holidays, cutoffs) in local time with the business’s IANA timezone. Storing everything in UTC is the mistake — working hours stored in UTC break every time DST changes.
What is the difference between UTC offset and IANA timezone?
- A UTC offset (
UTC+10) is a fixed number. It cannot represent daylight saving changes. - An IANA timezone (
Australia/Sydney) is a named region. The timezone database resolves the correct offset for any given date, including DST transitions. - Always store IANA names. Never store offsets.
How do I handle daylight saving time in a booking app?
You don’t handle it manually — your architecture handles it:
- Store business hours as local time (
09:00), not UTC. - Store the vendor’s IANA timezone.
- Convert to UTC only at the moment a specific booking is created.
- Let the timezone library (Luxon / Temporal) resolve the offset per date.
Done this way, a DST switch shifts the UTC timestamp automatically while the local appointment time stays fixed — which is exactly the correct behavior.
Why is new Date() bad for business logic?
new Date() returns the server’s (or browser’s) current time. Your booking rules belong to the vendor’s timezone, which is usually neither. Using it for cutoffs, availability, or “today” checks means your business logic changes behavior depending on where your server is deployed.
How do I generate available time slots across timezones?
In this exact order:
- Load the vendor’s IANA timezone.
- Load working hours (stored as local time).
- Generate slots in the vendor’s local time.
- Convert each slot to UTC.
- Display in the customer’s timezone.
Never generate slots in UTC first — business rules must never be evaluated in UTC.
Whose “today” should a booking system use?
- Availability, cutoffs, same-day rules → the vendor’s “today.”
- UI labels (“Today”, “Tomorrow”) → the customer’s “today.”
- Database → UTC timestamps only; UTC has no concept of “today.”
What timezone should a multi-vendor SaaS platform use?
None globally. Timezone is a per-vendor property. Each vendor record stores its own IANA timezone, and all business logic for that vendor executes in that timezone. The customer’s location only affects display.
Which library should I use for timezone handling in JavaScript?
- Temporal — the new standard, use it where available.
- Luxon — the best mature option today.
- Avoid building business logic on the native
Dateobject, and avoid Moment.js (in maintenance mode).
How do I test if my timezone handling is correct?
Create one booking on each side of a DST transition for the same vendor. If the local appointment time stays identical while the stored UTC timestamp shifts by one hour, your architecture is correct. If the local time drifts, your business rules are leaking into UTC.
Final Thoughts
Timezones aren’t a formatting problem. They’re a modelling problem.
Once you separate your system into three clear concepts —
- UTC for storage
- Vendor timezone for business rules
- Customer timezone for presentation
— most timezone bugs disappear on their own.
The remaining complexity, including daylight saving transitions, becomes the responsibility of the timezone database — not your application code.
Design your scheduling engine around these principles from day one, and it will scale from one local business to a global multi-vendor platform without ever rewriting its core time handling.
I’m Muneeb — a software developer who builds with AI. I write about the real engineering decisions behind production systems: booking platforms, APIs, and business software. If you’re building something with scheduling at its core and want it done right the first time, reach out at muneebdev.com.

Comments