Vibe Code a Booking App (Timezones Will Try to Kill You)

Build a scheduling app with AI — and handle the timezone, double-booking, and daylight saving bugs that break every naive implementation.

C
CodeIllusion Team
#vibe-coding #booking #saas #tutorial
Vibe Code a Booking App (Timezones Will Try to Kill You)

Booking looks simple and isn’t. The UI is a calendar and a list of slots. Underneath is the hardest ordinary problem in application development.

Every naive implementation works perfectly in one timezone in the middle of summer, and breaks the first time someone abroad books a meeting in late October.

The Rules That Prevent Most Bugs

Establish these before generating anything:

  1. Store instants in UTC. Every actual booking time, in UTC, always.
  2. Store timezone names, not offsets. "America/New_York", never "-05:00" — offsets change twice a year.
  3. Convert only at the edges. Display and input convert; nothing in between.
  4. Availability rules are local; bookings are absolute. “I’m free 9–5 Tuesdays” is a rule in the host’s local time. A booking is a fixed instant.

That fourth point is the subtle one. If you store availability in UTC, a host who’s free 9am–5pm local suddenly appears free 8am–4pm after the clocks change.

The Build

Next.js App Router, TypeScript, Tailwind, Supabase. A booking app where a host publishes availability and guests book slots.

Tables:

  • hosts: id, user_id, timezone text (IANA name), slot_duration_minutes, buffer_minutes
  • availability_rules: id, host_id, day_of_week 0-6, start_time time, end_time time — all in the host’s local time
  • bookings: id, host_id, guest_email, guest_timezone, starts_at timestamptz, ends_at timestamptz, status

All timestamptz columns store UTC. Use date-fns-tz for conversions. Never use raw Date arithmetic for timezone maths.

That last sentence prevents the single most common bug: new Date(x).setHours(9) operates in the server’s timezone, which is usually UTC in production and your local timezone in development. The code works on your machine and breaks when deployed.

Generating Slots

Write a server function that generates available slots for a date range:

  1. Read the host’s availability rules (local time) and timezone
  2. For each date in range, construct that day’s window in the host’s timezone using zonedTimeToUtc — so DST is applied for that specific date
  3. Divide into slots of slot_duration plus buffer
  4. Remove slots overlapping existing bookings
  5. Remove slots in the past relative to now
  6. Return UTC instants

Convert to the guest’s timezone only at render time.

Step 2 is where correctness lives. Converting per-date means each date gets the offset that actually applied on that date.

Double Booking

The obvious implementation has a race condition:

const conflicts = await checkOverlap(start, end)   // ← two requests both pass here
if (conflicts.length === 0) await createBooking(start, end)

Two people booking the same slot within milliseconds both see it free. Both bookings are created.

Fix it in the database:

Prevent double bookings with a Postgres exclusion constraint using btree_gist, so overlapping time ranges for the same host are rejected atomically. Handle the constraint violation in the application and return a clear “slot no longer available” message. Do not rely on an application-level availability check.

ALTER TABLE bookings ADD CONSTRAINT no_overlap
  EXCLUDE USING gist (
    host_id WITH =,
    tstzrange(starts_at, ends_at) WITH &&
  ) WHERE (status <> 'cancelled');

The database now guarantees it regardless of concurrency.

Display

Detect the guest’s timezone with Intl.DateTimeFormat().resolvedOptions().timeZone and let them override it with a searchable selector. Show the selected timezone prominently near the slot list. Display each slot in the guest’s timezone with the host’s local time underneath in smaller text.

Showing both times prevents the most common support message in every booking product.

Emails and Calendar Invites

On booking, email both parties. Each email must show the time in that recipient’s own timezone with the timezone name written out. Attach an .ics file with the event in UTC plus a TZID, so calendar clients localise correctly.

Send reminders 24 hours and 1 hour before, scheduled against the UTC instant.

Cancellation and Rescheduling

Allow cancellation via a signed token link, no login required. Cancelling frees the slot immediately. Rescheduling must be atomic — book the new slot before releasing the old one, so a failure doesn’t lose both.

Signed tokens matter: a cancellation link with a guessable booking ID lets anyone cancel anyone’s meeting.

Test the Cases That Break

Do these deliberately. They won’t surface on their own.

  • Guest in a different timezone from the host — times correct on both sides?
  • Guest in a half-hour offset zone (India, +5:30)
  • Booking across the March DST transition
  • Booking across the October DST transition
  • A host in a country with no DST, guest in one with it
  • Two simultaneous bookings for the same slot — one must fail
  • Booking near midnight in the guest’s timezone (date boundary)
  • Server timezone set to something unusual — does anything change?

That last one catches every place raw Date methods snuck in. Set TZ=Pacific/Kiritimati and run through the flow.

The Checklist

  • All instants in UTC
  • IANA timezone names stored, never offsets
  • Availability rules in host local time, converted per date
  • date-fns-tz used throughout, no raw Date arithmetic
  • Exclusion constraint prevents overlaps at the database level
  • Guest timezone detected and overridable
  • Both timezones shown on slots and emails
  • .ics uses UTC with TZID
  • Cancellation links signed
  • Rescheduling atomic
  • All eight test cases pass

Why This One Is Worth Building

Booking is the project where you learn that “it works” and “it’s correct” are different claims. Everything can look right and be wrong, and you won’t find out until October.

That’s a useful lesson to internalise cheaply, on a project where the consequence is a confused meeting rather than a lost payment.

Related reading:

Frequently Asked Questions

Why are timezones so hard in booking apps? +

Because a meeting exists at one instant but is described differently to each participant, and the offset between a timezone and UTC changes twice a year. Store instants in UTC, store the user's IANA timezone name separately, and convert only for display.

How do I prevent double bookings? +

Enforce it in the database, not the application. Application-level checks have a race window between checking availability and writing the booking. Postgres exclusion constraints on a time range reject overlaps atomically.

Should I store times in UTC? +

Yes, for the actual booking instants. But store the timezone name — 'Europe/London', not an offset like '+01:00' — alongside recurring availability rules, because offsets change with daylight saving and stored offsets go stale.

What breaks in booking apps at daylight saving? +

Recurring availability defined by offset shifts by an hour. Slots can duplicate or vanish on transition days. Test explicitly against the March and October transition dates — bugs here don't appear until the clocks change.

Tagged:

#vibe-coding #booking #saas #tutorial

Enjoyed this article?

Get more AI tool picks, coding tutorials, and no-code automation guides every week. No spam, ever.

Found this useful? Share it:

More in AI Coding Tools