Back to Explore
GuideVibeFix 编辑部Updated Oct 14, 2026

Timezones Are the Graveyard of Booking Features: Calendar Scheduling for Vibe-Coded Projects

DST switchover days, cross-timezone display chaos, broken recurrence rules — booking looks simple and hides the deepest pits. This guide covers the UTC-storage iron rule, the IANA timezone database, floating vs. absolute time, RRULE, transactional conflict detection, and 12 mandatory test cases for a booking system that survives daylight saving.

Calendar scheduling cover art: planner and world clock motif

Prologue: The Simplest-Looking Feature Has the Deepest Pits

Every developer who has built a booking feature thought, before starting, that it was a "three-day job": pick a date, pick a slot, save to the database, send a reminder. After starting, they discover it sits at the intersection of time, concurrency, and distributed systems.

Three classic production incidents. Incident one: on US daylight-saving switchover day (2 AM jumps straight to 3 AM on the second Sunday of March), your system conjures bookings for "2:30 AM" — a moment that doesn't exist that day — and two users get scheduled into the same nonexistent slot. Incident two: your user in Beijing books a meeting with a client in New York; the page shows Beijing time, the reminder email says New York time, and when the client joins on time, your user is still at lunch — the display layer and the notification layer used two different timezone logics. Incident three: a user sets "every Wednesday 10 AM" recurring; after New Year everything scrambles — your recurrence implementation jumps from Wednesday Dec 30 straight to Jan 6, which looks right until you realize Jan 1 was a Friday that year and the real "every Wednesday" should have been Jan 7… your RRULE parser tripped on the year boundary.

The common root cause: treating time as strings and timezones as a display concern. Time is the core domain model of a booking system; timezones are its sharpest edge. This guide breaks booking into five hard problems — build-vs-buy, timezone engineering, the availability engine, the fulfillment loop, and the test checklist — and working through them gives you a booking system that survives daylight saving.

Build vs. Buy: Cal.com or Full Custom?

Before writing code, answer a strategic question: should you build this wheel at all? The booking space has a mature open-source answer — Cal.com (MIT licensed, self-hostable, embeddable). The decision table:

  • Self-host / embed Cal.com: if your booking needs are "standard shape" — 1:1 meetings, team round-robin, paid bookings — just use it. A 2C4G VPS runs it fine, with iframe and API embedding options. The cost is a customization ceiling: its UI and data model are designed for "meeting scheduling." If what you need is booking with complex business rules (medical triage, venue booking with resource locking), modifying it hurts more than rewriting.
  • Build it yourself: when booking is your core differentiator (the booking logic is your product), or you need booking deeply embedded in an existing flow (book → pay → redeem → review as one chain). The cost: you hand-step every pitfall in the rest of this guide — which is exactly what this guide is for.

A rule of thumb: if you can describe your booking needs in three sentences, use Cal.com; if you're still explaining after five minutes, build it. The worst outcome is the middle state — customizing Cal.com halfway, discovering it won't bend, then rebuilding from scratch. Two jobs, both wasted.

Also mind licensing and cost: self-hosting Cal.com is free, but you own upgrades, backups, and availability; its Cloud plan charges per user and is often cheaper than self-building for small teams. Don't self-host a service you have no time to operate to "save money" — ops cost is the line most often missing from the build-it-yourself quote.

Timezone Engineering: Always Store UTC, Convert on Display

This is the most important section of the whole guide, and the highest-incident one. The data flow:

Timezone data flow: user enters local time plus timezone, parsed and validated, stored as UTC, converted back to local on display; floating vs. absolute time split Figure 1: Timezone engineering data flow. The iron rule: always store UTC, convert to local on display; floating and absolute times use different models.

Iron Rule 1: The Storage Layer Only Speaks UTC

What goes in the database? Always UTC timestamps. Postgres: timestamptz. MySQL: DATETIME holding UTC (MySQL's TIMESTAMP has implicit-conversion gotchas; for vibe projects the recommendation is DATETIME-in-UTC with an application-layer convention). Never store a bare local time like "2026-10-20 10:00" — a timestamp string without timezone info is a time bomb that detonates at every cross-timezone query, DST calculation, and data migration.

But UTC alone isn't enough. Bookings have a second dimension: the timezone the event happens in. A user in New York books "every Wednesday 10 AM (New York time)": store UTC 2026-10-21T14:00:00Z and event_timezone = 'America/New_York'. Why? After the DST switch, "every Wednesday 10 AM New York time" maps to a different UTC (EDT is UTC-4, EST is UTC-5). Store only UTC and your recurring bookings drift by an hour after the first Sunday of November — the classic slow-bleed bug of booking systems, which users don't notice for half a year.

-- Bookings table: UTC time + event timezone, both columns mandatory
CREATE TABLE bookings (
  id            UUID PRIMARY KEY,
  starts_at     TIMESTAMPTZ NOT NULL,  -- UTC; sorting & conflict detection use this
  ends_at       TIMESTAMPTZ NOT NULL,
  event_tz      TEXT NOT NULL,         -- IANA zone, e.g. America/New_York
  rrule         TEXT,                  -- recurrence rule (next section)
  status        TEXT NOT NULL DEFAULT 'pending',
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Iron Rule 2: Use the IANA Timezone Database — Never Compute Offsets Yourself

A timezone is not a fixed offset like "UTC+8" — a timezone is rules; an offset is the result. Beijing happens to be fixed at UTC+8, but New York oscillates between UTC-4 and UTC-5 through the year, and governments have changed switchover dates retroactively. Use tzdata via mature implementations: Python's zoneinfo, Node's luxon/date-fns-tz, Go's time.LoadLocation. Hand-rolling a "daylight saving detector" is self-harm — tzdata ships updates several times a year because governments change rules on a whim.

There's a matching frontend trap: new Date() uses the device's local timezone, which may differ from your business timezone. The correct posture: the frontend only collects "local time + IANA timezone name"; all conversion happens server-side. Pass the zone name from Intl.DateTimeFormat().resolvedOptions().timeZone to the backend — never the numeric offset, which is wrong at DST boundaries.

Iron Rule 3: Separate "Floating Time" from "Absolute Time"

The most underrated lesson in timezone engineering. Absolute time (meetings, bookings, flights) is anchored to a point on the timeline and renders differently per observer timezone; floating time (birthdays, holidays, "take the pill at 8 AM daily") is anchored to the local wall clock and never converted.

99% of a booking system is absolute time, but two exceptions trip everyone up. One: business hours ("Mon–Fri 9:00–18:00") — floating time, modeled on the merchant's wall clock (store the "09:00" string + merchant timezone). Never convert it to UTC for storage, or the merchant's hours drift at every DST switch. Two: birthday/anniversary reminders — store a date, not a timestamptz, or cross-timezone users get their birthday reminder "yesterday."

DST Boundary Test Checklist

DST switchover day is the final exam for booking features. Before launch, seed your test environment with full data for those days:

  • Spring forward: in the US in 2026, March 8 — 2:00 AM jumps to 3:00 AM; "2:30 AM" doesn't exist. Test: a user trying to book 2:30 must be rejected or explicitly warned, never silently stored as 3:30.
  • Fall back: in the US in 2026, November 1 — 2:00 AM happens twice (1:30 AM EDT and 1:30 AM EST are two different UTC instants). Test: booking "Nov 1, 1:30 AM" must force the user to pick which 1:30; ambiguity here means double-booking.
  • Recurrence spanning the switch: "every Wednesday 10 AM (New York)" crossing Nov 1 must still be 10 AM local after the switch, not drifted to 9 AM because UTC stayed constant.

Remember: DST bugs detonate half a year after they're written. Code written in March explodes in November, long after you've forgotten that logic. So test data must "time-travel" to switchover days — testing only "next Wednesday" proves nothing.

The Availability Engine: Computing "Can This Be Booked" Correctly

Timezones answer "what time it is"; the availability engine answers "which times are bookable." Four components: business-hours modeling, buffer time, recurrence rules, conflict detection.

Modeling Business Hours

Business hours are floating time (see above), modeled on the merchant timezone's wall clock. Recommended structure: per-weekday slot arrays, plus an override table for special dates (holidays, ad-hoc closures).

// Business hours: weekly template + per-date overrides
{
  "timezone": "Asia/Shanghai",
  "weekly": {
    "1": [["09:00","12:00"],["13:30","18:00"]],  // Monday
    "2": [["09:00","12:00"],["13:30","18:00"]],
    ...
    "0": []                                      // Sunday closed
  },
  "overrides": {
    "2026-10-25": [],                            // ad-hoc closure: override to empty
    "2026-10-01": [["10:00","16:00"]]            // holiday short hours
  }
}

Key design: overrides always win over the weekly template — check the override table first when resolving a day's availability. Simple enough to store as JSONB; vibe projects shouldn't rush into a pile of relational tables.

Buffer Time: 15 Minutes on Each Side

Buffer time is the easiest thing to "optimize away" in a booking system — and the last thing that should be. A 10:00–10:30 booking followed by 10:30–11:00 leaves the provider no time to drink water. Implementation-wise, buffers must participate in conflict detection: a booking's "occupied interval" is [starts_at − buffer_before, ends_at + buffer_after], not [starts_at, ends_at].

A litmus test: if your conflict detection ignores buffer time, your system oversells from day one — except it oversells human energy rather than seats, which is harder to detect and more damaging.

Recurrence with RRULE: Don't Invent Your Own

Express recurring bookings ("every Wednesday 10 AM", "the 1st of each month") with RRULE (RFC 5545) — the calendar world's de facto standard, understood by Google Calendar and Apple Calendar alike. Python has dateutil.rrule, JS has rrule.js. Never hand-write a "day of week" parser — you won't survive "the third Friday of each month."

Two RRULE field notes. First, expansion needs a boundary: to answer "which instances exist in the next 3 months," expand the RRULE in memory and filter — never try to parse RRULE inside SQL. Second, per-instance reschedule/cancel uses RECURRENCE-ID semantics: don't mutate the whole rule; create an "exception record" for that instance (moved to what time / cancelled that day), and overlay exceptions at query time. That's how Google Calendar does it, and it's the only approach that has survived contact with reality.

Conflict Detection Algorithm (Copy-Paste Ready)

Conflict detection is the heart of a booking system, and the logic is surprisingly simple: two intervals overlap ⟺ A.start < B.end AND B.start < A.end. Memorize this biconditional — it eliminates 90% of hand-rolled bugs (people write "containment" checks and miss partial overlaps).

-- Does new booking [p_start, p_end) conflict with confirmed ones (incl. buffers)?
-- Overlap biconditional: existing.starts_at < p_end AND existing.ends_at > p_start
SELECT COUNT(*) > 0 AS has_conflict
FROM bookings
WHERE resource_id = $1
  AND status = 'confirmed'
  AND starts_at < $3   -- $3 = p_end + buffer_after
  AND ends_at   > $2;  -- $2 = p_start - buffer_before

But SQL is only "detection" — the real trap is concurrency: two users both see "10:00 available," both submit, both pass detection, and you've double-booked. The fix: detection + insert in one transaction, with a row lock on the resource (SELECT ... FOR UPDATE on the resource row, or idempotency via unique constraints). The most common vibe-project failure is "a network round-trip between check and write" — wrap both steps in one transaction and double-booking disappears.

The booking state machine deserves its own diagram — it defines every legal path in a booking's lifetime:

Booking state machine: pending to confirmed to completed, cancelled, or no-show, with T-24h email and T-2h SMS reminder touchpoints Figure 2: The booking state machine. State transitions belong inside transactions: confirm and lock the slot atomically, or you oversell.

A state machine's practical value is that it forces you to answer every "and then what": pending booking never handled — auto-cancel or auto-confirm? Confirmed booking, user never shows — mark no-show, dock credit score? Cancelled slot — released when (immediately)? Every arrow maps to code; every missing arrow is a bug nursery.

The Fulfillment Loop: Booking Is Just the Beginning

Many vibe projects stop at "bookable" and end up with 40% no-show rates and furious merchants. A booking's value isn't in being booked — it's in being fulfilled.

Confirmation & Reminders: Email + SMS, Two Channels

The golden reminder combo: T-24h email + T-2h SMS. Email carries the details (time, location, reschedule link); SMS carries the nudge (short and impossible to miss). Why two channels? Email open rates top out around 30%; SMS clears 90% — but SMS is expensive. Cheap email at 24 hours, expensive SMS at 2 hours: the cost-effectiveness optimum.

One timezone subtlety in reminders: "24 hours ahead" is computed in absolute time (UTC minus 24h), but the time shown in the message renders in the user's local timezone. Confuse the two and you get the classic disaster: the reminder says 10 AM, the user reads it as 10 AM their time, but it meant 10 AM the other party's time. The scheduler (cron / delayed queue) scans "confirmed, unreminded bookings in the next 24h" comparing in UTC — non-negotiable.

Reschedule / Cancellation Policy: In Code, Not in Support Scripts

Policies must be codified: how far ahead is free cancellation (e.g., 24h), reschedule count caps, how many no-shows trigger a ban. Example: "cancellations within 24h of start incur a 50% fee; within 2h count as no-show." Put these in a config table; support executes, never interprets. A booking system with fuzzy policies turns support staff into human state machines — exhausted, and always accused of double standards.

No-Show Handling: The Seed of a Credit System

Unhandled no-shows mean reliable users subsidize unreliable ones. A lightweight ladder: first no-show → warning; second → 30 days restricted to "next-day" slots only (capping merchant loss); third → booking privileges frozen. Record it in the user profile, where the fraud layer can use it too — a user who no-shows merchant A will probably no-show merchant B.

Google Calendar Two-Way Sync: Avoid If You Can

Two-way sync is booking's famous "looks beautiful": you must handle change push from Google's side (webhooks), conflict resolution (both sides edited simultaneously — who wins?), and delete semantics (does deleting on Google mean cancel or hide?). Field advice: ship one-way sync first (your system → Google Calendar) — create the event via the Calendar API, store the event ID, update it on reschedule. Touch two-way sync when you have a dedicated backend engineer — it's a standalone project, not "a feature."

The Test Checklist: 12 Mandatory Cases

Twelve must-pass cases before launch. Write them as automated tests, not a manual click-through — timezone bugs detonate half a year after they're written; manual testing can't guarantee regression coverage.

  • Cross-midnight: a 23:30–00:30 booking must land its end date on the next day; billing/stats attribute by start date, not end date.
  • Cross-month: a "monthly" recurrence on Jan 31 lands on Feb 28 next month (not Mar 3) — verify your RRULE library's default.
  • Spring-forward DST day: 2026-03-08 (US) — a 2:30 AM booking must be rejected or explicitly flagged, never silently stored as 3:30.
  • Fall-back DST day: 2026-11-01 (US) — 1:30 AM occurs twice; the user must pick one, no ambiguity allowed.
  • Recurrence across the switch: "every Wednesday 10 AM (New York)" crossing Nov 1 must remain 10 AM local afterward.
  • Leap-year Feb 29: a "yearly on Feb 29" recurrence in a common year — Feb 28 or skip? Product must decide, code must implement, tests must cover.
  • Year-boundary recurrence: "every Wednesday" crossing Dec 31 — don't miscalculate the ISO week at the year edge.
  • Concurrent double-booking: two requests for the same slot at once — exactly one must succeed. Verify with 50 parallel requests.
  • Buffer participates in conflicts: 10:00–10:30 (15-min trailing buffer) vs. 10:30–11:00 must conflict.
  • Reschedule into an occupied slot: rescheduling is "cancel + rebook" — the new booking must pass conflict detection, no skipping.
  • Slot released immediately on cancel: after the cancel transaction commits, the slot must be bookable again — no stale cache.
  • Reminder timezone correctness: reminders to cross-timezone users render the meeting time in the recipient's zone — assert the exact string in a fixed test case.

One-line summary: booking quality = timezone correctness × concurrency safety × fulfillment loop — if any factor is zero, the product is zero. Timezones rest on UTC + IANA + dual-column storage; conflict detection tops out at "overlap biconditional + transactional row locks"; reminders close with the T-24h/T-2h dual-channel combo. Swing those three moves cleanly and your booking system survives its first daylight saving.

Views 0Comments 0

Comments (0)

ME
0/1000
Loading comments...
Browse projectsPublish your project

Related articles

Studio microphone close-up representing voice feature integration: capture, transcription, synthesis and realtime dialogue for vibe projects
Guide
Give Your Product a Voice: Voice Feature Integration for Vibe Projects

Voice demos are easy; shippable voice features are pipeline engineering. This guide covers picking the right of three shapes (STT/TTS/realtime), the 400ms latency budget split across capture→STT→LLM→TTS→playback, Whisper vs streaming STT, the TTS quality/latency/cost triangle, interruption and turn state machines, five browser pitfalls, cost modeling with anti-abuse, and a 10-item launch checklist.

Tool TipsBackend EngineeringProject Building
Anti-fraud cover art: shield and network security motif
Guide
Don't Wait Until You're Fleeced: The Anti-Fraud Playbook for Vibe-Coded Projects

CAPTCHAs can't stop business-layer fraud. This hands-on guide gives you a four-layer defense system (registration → device → behavior → business), a copy-paste weighted risk-scoring table, a graduated three-tier response strategy, and a 15-minute weekly fraud patrol checklist — making fraud unprofitable without hurting legitimate users.

Application SecurityBackend EngineeringSecurity & Privacy
Charge lifecycle flowchart for one AI API call: idempotency check, row lock, pre-auth hold, execution, then settle on success or rollback on failure
Guide
Metered Billing in the AI Era: Designing and Reconciling a Credits Ledger

Your users' credits are not a balance column — they're a journal. This guide builds a double-entry credits ledger for AI SaaS: immutable journal schema you can copy, idempotent deduction with row locks, two-phase pre-auth/settle for unpredictable token usage, refund and expiry write-backs, a three-way daily reconciliation, negative-balance circuit breakers, and an anti-abuse checklist.

Payments & MonetizationBackend EngineeringIndie Development