Multi-Timezone Booking Engine
Target: Claude Code • Cursor • Windsurf • Devin
326 Lines • Markdown + DDL
Calendly & Cal.com Replacement — Entire Agent Prompt Specification
This is the verbatim production plan (calendly-booking-engine.plan.md). Copy and paste this directly into your coding agent's Planning Mode prompt, or attach it as a workspace file before starting execution.
calendly-booking-engine.plan.md
# Blueprint: Availability & Booking Engine (Cal.com / Calendly Clone)
**Version:** 1.9.0 (Zero-Deadlock Autonomous Phased Verification Edition)
**Target Agents:** Claude Code, Cursor, Windsurf, Devin, Grok
**Methodology:** Strict Planning Mode, Multi-Timezone UTC Math, Per-Host Lock + GIST Exclusion Constraints, Pure Slot Engine, Autonomous Phased Test Gates
---
## Execution Protocol for the Agent (Autonomous Phased Verification)
You are an autonomous principal engineer. You must execute this plan sequentially from Phase 1 through Phase 5 without requiring manual human prompts between phases:
1. **Phased State Machine:** Implement strictly one phase at a time under Section 5. Never scaffold, modify, or jump ahead to future phases out of order.
2. **Automated Test Gate Verification:** At the end of each phase, execute that phase's designated verification test command (e.g., `pnpm run test:gate1` or `pnpm test`).
3. **Self-Healing Loop:** If a verification test fails, inspect the stack trace and error message, apply fixes to the implementation code, and re-run until passing (up to 3 remediation cycles). Never weaken or delete assertions to force a green test.
4. **Autonomous Progression:** When Phase [N] tests pass with 0 errors, log a concise `[PHASE N COMPLETE - GATE PASSED]` summary to stdout and immediately transition to Phase [N+1].
5. **Final Delivery:** Stop only after Phase 5 integration tests and `pnpm run build` pass cleanly. Deliver the final test report and instructions to launch the local application with `pnpm run dev`.
*(Note: If the user explicitly commands a checkpoint mode like "pause for review", pause and await user approval before advancing; otherwise, proceed autonomously.)*
---
## 1. Executive Summary & Tech Stack Constraints
A multi-timezone meeting scheduling platform that lets hosts publish weekly availability schedules, handles custom buffer times between appointments, prevents double-booking via per-host row locking and a database-level GIST exclusion constraint, and generates RFC 5545 `.ics` calendar files.
### Architectural Constraints:
- **Runtime & Framework:** Node.js 20.11+ (or 22 LTS) with Next.js 14.2+ (App Router). In Next.js 15+, dynamic server helpers like `headers()` return Promises, so all header reads must use `await headers()`.
- **Language:** TypeScript 5+ (Strict Mode).
- **Database:** PostgreSQL 14+ (Docker / Local / Neon / Supabase) with UTC `TIMESTAMPTZ` enforcement; requires the `btree_gist` extension.
- **Database Driver:** `postgres` (`postgres-js`) configured with `{ prepare: false }`. This is mandatory for compatibility with transaction poolers (Supabase PgBouncer on port 6543) and Neon while supporting interactive transactions for `SELECT ... FOR UPDATE`.
- **ORM & Migrations (No `drizzle-kit push`):** Drizzle ORM. Because Drizzle's TypeScript schema DSL cannot natively express PostgreSQL `EXCLUDE USING gist` constraints, running `drizzle-kit push` (`db:push`) will silently skip or drop the double-booking constraint. **Never define or run `db:push`.** The single source of truth for DDL is `src/db/migrations/0001_init.sql` applied idempotently via `src/db/migrate.ts` (`pnpm run db:migrate`). `src/db/schema.ts` provides the TypeScript query layer mirroring the tables.
- **Isolated Test Database:** Tests must **never** run against or wipe the development database (`DATABASE_URL`). `tests/setup/pg-container.ts` must connect to `DATABASE_URL_TEST` if set, or derive a dedicated test database (e.g. `calendly_test` created automatically on the local Postgres server, or a Testcontainers PostgreSQL container).
- **Pinned Test Tooling:** Pin `vitest@^3.0.0` and `@testcontainers/postgresql@^10.0.0` so the test runner works seamlessly on both Node 20 and Node 22.
- **Timezone Library & Hydration Safety:** `date-fns@^3` and `date-fns-tz@^3` (using `fromZonedTime` and `formatInTimeZone`). Validate host and guest timezones using `try { new Intl.DateTimeFormat(undefined, { timeZone }); } catch { ... }` in Zod schemas. To prevent React SSR/client hydration mismatches caused by differing ICU timezone lists between Node.js and browsers, compute `const timezones = Intl.supportedValuesOf('timeZone')` on the server in `app/[username]/[slug]/page.tsx` and pass it as a prop to `<TimezoneSelector timezones={timezones} />`. Detect the guest's default browser timezone (`Intl.DateTimeFormat().resolvedOptions().timeZone`) inside a `useEffect` hook after mount.
- **Double-Booking & Deadlock Prevention (`23P01` + `40P01`):**
1. Database enforces `EXCLUDE USING gist (host_id WITH =, tstzrange(blocked_start, blocked_end, '[)') WITH &&) WHERE (status = 'confirmed')`.
2. Inside `bookSlot`, serialize concurrent booking attempts for the same host by acquiring a per-host row lock (`SELECT id FROM hosts WHERE id = $1 FOR UPDATE`) at the start of the transaction before checking availability and inserting. This prevents concurrent GIST index page lock upgrades from deadlocking and ensures a 5-way race finishes in under 200ms.
3. Catch PostgreSQL `SQLSTATE 23P01` (`exclusion_violation`, via `err.code === '23P01' || err.cause?.code === '23P01'`) and map it to `{ success: false, error: 'SLOT_TAKEN' }`.
4. If PostgreSQL raises `SQLSTATE 40P01` (`deadlock_detected`), retry the transaction up to 4 times with jittered backoff (15–50ms); if retries are exhausted or a conflict is found, return `{ success: false, error: 'SLOT_TAKEN' }`.
- **Zero-Account Rate Limiting & Safe Client IP:**
- `src/lib/rate-limiter.ts` implements a built-in, zero-account in-memory sliding-window rate limiter (max 5 booking attempts per IP per 60 seconds) by default, and optionally upgrades to `@upstash/ratelimit` only when `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are configured.
- `src/lib/client-ip.ts` reads `x-real-ip` first, only trusts `x-forwarded-for` when `process.env.TRUST_PROXY === '1'` (taking the rightmost hop), and falls back to `'127.0.0.1'` so spoofed `X-Forwarded-For` headers cannot bypass rate limits by default.
- **Host Dashboard Protection:** `app/dashboard/availability/page.tsx` and its update action require a host passcode/token matching `process.env.ADMIN_TOKEN` (defaulting to `'demo-admin-token'` in `.env.example`) via an `httpOnly` cookie or header so unauthenticated guests cannot overwrite the host's schedule.
- **Booking Core & Entry Points:** `server/booking/book-slot.ts` exports `bookSlot(input, deps)` and holds all booking logic. It is a plain server module with **no `"use server"` directive**, so `deps` (clock, rate limiter, IP, db) can never be supplied by a browser. Three entry points wire it up:
- `server/actions/book-slot-action.ts` (`"use server"`): accepts only `input`, reads IP via `clientIp(await headers())`, calls `bookSlot(input, { ip })`.
- `app/api/bookings/route.ts`: `POST` endpoint that parses JSON, reads IP, calls `bookSlot`, and returns HTTP 201 (or 409 on `SLOT_TAKEN`, 429 on `RATE_LIMITED`, 400 on validation error).
- `app/api/bookings/[id]/ics/route.ts`: `GET` endpoint that serves the booking's RFC 5545 `.ics` file with `Content-Type: text/calendar; charset=utf-8` and `Content-Disposition: attachment; filename="invite.ics"`.
- **File Limit:** Under 200 lines per modular file.
---
## 2. Relational Schema (PostgreSQL DDL)
> Apply idempotently via raw SQL in `src/db/migrations/0001_init.sql` through `src/db/migrate.ts`.
```sql
-- Enable GIST extension for UUID and scalar equality support
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- Hosts / Users
CREATE TABLE IF NOT EXISTS hosts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(100) UNIQUE NOT NULL,
full_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
timezone VARCHAR(100) NOT NULL DEFAULT 'UTC', -- e.g. 'America/New_York'
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Event Types (e.g. 30-min intro call)
CREATE TABLE IF NOT EXISTS event_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL,
duration_minutes INT NOT NULL DEFAULT 30 CHECK (duration_minutes > 0),
before_buffer_minutes INT NOT NULL DEFAULT 5 CHECK (before_buffer_minutes >= 0),
after_buffer_minutes INT NOT NULL DEFAULT 10 CHECK (after_buffer_minutes >= 0),
min_notice_minutes INT NOT NULL DEFAULT 120 CHECK (min_notice_minutes >= 0),
max_horizon_days INT NOT NULL DEFAULT 60 CHECK (max_horizon_days > 0),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_host_slug UNIQUE (host_id, slug),
CONSTRAINT uq_event_types_host UNIQUE (id, host_id)
);
-- Weekly Availability Windows (stored as local TIME in host's timezone)
CREATE TABLE IF NOT EXISTS availability_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=Sunday
start_time TIME NOT NULL, -- e.g. 09:00:00
end_time TIME NOT NULL, -- e.g. 17:00:00 (Overnight windows split across days)
CHECK (end_time > start_time)
);
-- Specific-date overrides (days off, one-off hours, holidays)
CREATE TABLE IF NOT EXISTS date_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
override_date DATE NOT NULL,
is_unavailable BOOLEAN NOT NULL DEFAULT FALSE,
start_time TIME,
end_time TIME,
CHECK (
is_unavailable = TRUE
OR (start_time IS NOT NULL AND end_time IS NOT NULL AND end_time > start_time)
),
CONSTRAINT uq_host_date_override UNIQUE (host_id, override_date)
);
-- Booked Appointments
CREATE TABLE IF NOT EXISTS bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type_id UUID NOT NULL,
host_id UUID NOT NULL,
guest_name VARCHAR(255) NOT NULL,
guest_email VARCHAR(255) NOT NULL,
guest_timezone VARCHAR(100) NOT NULL,
guest_notes TEXT,
start_time TIMESTAMPTZ NOT NULL, -- UTC; actual meeting start
end_time TIMESTAMPTZ NOT NULL, -- UTC; actual meeting end
-- Immutable buffer snapshots captured on insert
blocked_start TIMESTAMPTZ NOT NULL, -- start_time - before_buffer_minutes
blocked_end TIMESTAMPTZ NOT NULL, -- end_time + after_buffer_minutes
status VARCHAR(20) NOT NULL DEFAULT 'confirmed'
CHECK (status IN ('confirmed', 'cancelled', 'rescheduled')),
cancel_token UUID NOT NULL DEFAULT gen_random_uuid(),
rescheduled_from_id UUID REFERENCES bookings(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (end_time > start_time),
CHECK (blocked_start <= start_time),
CHECK (blocked_end >= end_time),
CONSTRAINT fk_bookings_event_type_host
FOREIGN KEY (event_type_id, host_id)
REFERENCES event_types(id, host_id)
ON DELETE RESTRICT,
CONSTRAINT uq_bookings_cancel_token UNIQUE (cancel_token)
);
-- Exclusion constraint: prevents overlapping blocked ranges for confirmed bookings of the same host (idempotent block).
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'no_overlap_per_host'
) THEN
ALTER TABLE bookings
ADD CONSTRAINT no_overlap_per_host
EXCLUDE USING gist (
host_id WITH =,
tstzrange(blocked_start, blocked_end, '[)') WITH &&
) WHERE (status = 'confirmed');
END IF;
END $$;
```
---
## 3. Modular File System Architecture (Hard 200-Line Cap)
```
├── drizzle.config.ts # Drizzle configuration
├── vitest.config.ts # Vitest test runner configuration
├── .env.example # DATABASE_URL, DATABASE_URL_TEST, NEXT_PUBLIC_APP_URL, ADMIN_TOKEN
src/
├── app/
│ ├── [username]/[slug]/page.tsx # Public booking calendar Server Component (< 140 lines)
│ ├── dashboard/availability/page.tsx # Host weekly schedule editor (< 160 lines)
│ ├── api/bookings/route.ts # POST booking HTTP endpoint (< 60 lines)
│ └── api/bookings/[id]/ics/route.ts # GET RFC 5545 .ics download endpoint (< 60 lines)
├── components/
│ ├── calendar/
│ │ ├── booking-shell.tsx # Client state wrapper coordinating date, slot, form & confirmation (< 140 lines)
│ │ ├── day-grid.tsx # Day picker (< 110 lines)
│ │ ├── time-slot-column.tsx # Time slot pills (< 130 lines)
│ │ └── guest-details-form.tsx # Guest input form (< 120 lines)
│ ├── booking/
│ │ ├── booking-confirmed-card.tsx # Confirmation screen + .ics download link (< 90 lines)
│ │ └── timezone-selector.tsx # Server-seeded IANA timezone dropdown (< 85 lines)
│ └── ui/
│ └── button.tsx, dialog.tsx # Atomic UI primitives
├── server/
│ ├── actions/
│ │ ├── book-slot-action.ts # "use server" booking wrapper (< 40 lines)
│ │ └── availability-action.ts # "use server" host schedule updater with ADMIN_TOKEN check (< 80 lines)
│ ├── booking/
│ │ └── book-slot.ts # Core booking transaction with host lock & 23P01/40P01 handling (< 190 lines)
│ └── slot-engine/
│ ├── compute-slots.ts # Pure calculation algorithm (< 180 lines)
│ ├── get-available-slots.ts # Padded database loader (< 110 lines)
│ └── timezone-converter.ts # Timezone utilities (< 110 lines)
├── db/
│ ├── client.ts # postgres-js connection with prepare: false (< 50 lines)
│ ├── migrate.ts # Raw SQL migration runner (< 60 lines)
│ ├── seed.ts # Idempotent 'demo-host' & '30-min-intro' seeder (< 80 lines)
│ ├── schema.ts # Drizzle table schemas (< 180 lines)
│ └── migrations/
│ └── 0001_init.sql # Raw SQL DDL with btree_gist and GIST constraint
└── lib/
├── client-ip.ts # Safe IP header extraction (< 40 lines)
├── ics-generator.ts # RFC 5545 generator with SEQUENCE tracking (< 120 lines)
├── rate-limiter.ts # Built-in sliding-window limiter + optional Upstash (< 70 lines)
└── validation.ts # Zod validation schemas (< 80 lines)
tests/
├── setup/
│ └── pg-container.ts # Isolated test DB harness: DATABASE_URL_TEST, dedicated test DB, or Testcontainers (< 85 lines)
├── slot-engine.test.ts # Gate 1: Pure unit tests for DST, buffers, timezones
├── ics-generator.test.ts # Gate 1: RFC 5545 compliance & line folding tests
├── slot-loader.test.ts # Gate 2: Boundary padding integration tests
└── booking-concurrency.test.ts # Gate 2: 5-way concurrent race & single-use reschedule token tests
```
---
## 4. State Machine & Business Logic Invariants
1. **Per-Host Serialization + GIST Exclusion Constraint:**
- Every confirmed booking occupies `tstzrange(blocked_start, blocked_end, '[)')`.
- At the start of `bookSlot`'s transaction, execute `SELECT id FROM hosts WHERE id = $1 FOR UPDATE` to serialize concurrent writes for the same host and prevent GIST index page deadlocks (`40P01`).
- The database also enforces `no_overlap_per_host` via GIST exclusion (`SQLSTATE 23P01`). `bookSlot` catches `23P01` and retries up to 4 times on `40P01` before returning `{ success: false, error: 'SLOT_TAKEN' }`.
2. **Buffer Stacking Invariant:** Blocked spans include `before_buffer_minutes` and `after_buffer_minutes`. Because the database prevents blocked spans from overlapping, the minimum gap between two consecutive meetings is the first meeting's after-buffer plus the second meeting's before-buffer.
3. **Pure Slot Calculation (Zero I/O):**
- Candidate slots are stepped on a fixed 30-minute (or `duration_minutes`) grid in host wall-clock time.
- **DST Round-Trip Check:** For each candidate, convert `(hostDate, localTime)` to UTC via `fromZonedTime(..., hostTimezone)`. Then format back using `formatInTimeZone(candStartUtc, hostTimezone, 'HH:mm')`. If the formatted time does not match `localTime`, the time fell into a non-existent spring-forward hour and is skipped.
- **UTC Duration Invariance:** `candEndUtc = new Date(candStartUtc.getTime() + duration_minutes * 60_000)`. Never compute local end time and convert back across DST shifts.
- Returns a flat array of UTC slots `{ startUtc: string; endUtc: string }[]`.
4. **Deterministic Testing:** `computeSlots` and `bookSlot` accept `now` as an explicit parameter so tests are 100% deterministic regardless of current date or system clock.
5. **Reschedule State Machine & UID Preservation:**
- Rescheduling locks the old booking row via `SELECT ... FOR UPDATE` on `cancel_token` (after locking the host row), verifies `status = 'confirmed'`, marks the old booking `'rescheduled'`, and inserts the new booking with `rescheduled_from_id = oldBooking.id` inside the same transaction.
- In `.ics` generation, if `rescheduled_from_id` is present, preserve the **original booking's UID** (`booking-${rescheduledFromId}@promptscale.dev`) and increment `SEQUENCE` (e.g. `SEQUENCE:1`).
---
## 5. Phase-by-Phase Implementation Prompts for Agents
### Phase 1: Database Setup, Raw SQL Migrations & Isolated Test Harness
```
Execute Phase 1:
1. Initialize the Next.js 14+ TypeScript project in the current directory (requiring Node >= 20.11):
pnpm create next-app@14 . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
pnpm add drizzle-orm postgres date-fns@^3.6.0 date-fns-tz@^3.2.0 zod @upstash/ratelimit ical.js
pnpm add -D drizzle-kit vitest@^3.0.0 @testcontainers/postgresql@^10.0.0 tsx @types/node
2. Define these exact scripts in package.json (never use drizzle-kit push because it skips GIST exclusion constraints):
- "db:migrate": "tsx src/db/migrate.ts"
- "db:seed": "tsx src/db/seed.ts"
- "test:gate1": "vitest run tests/slot-engine.test.ts tests/ics-generator.test.ts"
- "test:gate2": "vitest run tests/slot-loader.test.ts tests/booking-concurrency.test.ts"
- "test:concurrency": "vitest run tests/booking-concurrency.test.ts"
- "test": "pnpm run test:gate1 && pnpm run test:gate2"
3. Create .env.example with DATABASE_URL, DATABASE_URL_TEST, NEXT_PUBLIC_APP_URL="http://localhost:3000", and ADMIN_TOKEN="demo-admin-token".
4. Create src/db/client.ts using postgres-js with { prepare: false }.
5. Create src/db/migrations/0001_init.sql with the complete idempotent DDL from Section 2, including CREATE EXTENSION IF NOT EXISTS btree_gist and the no_overlap_per_host GIST exclusion constraint.
6. Create src/db/migrate.ts to apply 0001_init.sql against a target connection string.
7. Create src/db/schema.ts mirroring the tables in Drizzle syntax for type-safe queries.
8. Create src/db/seed.ts: idempotently upsert 'demo-host' in 'America/New_York' with a 30-min event ('30-min-intro', 5m before-buffer, 10m after-buffer, 120m min_notice, 60d max_horizon) and Mon-Fri 09:00-12:00, 13:00-17:00 availability.
9. Create tests/setup/pg-container.ts: NEVER run tests against the dev DATABASE_URL directly. Connect to DATABASE_URL_TEST if provided, or create/connect to a dedicated `calendly_test` database (or isolated `test_run` schema) on the Postgres host, or start a Testcontainers PostgreSQL container. Run migrate and seed against the isolated test DB, and verify that inserting an overlapping confirmed booking raises SQLSTATE 23P01.
Keep all files under 200 lines. Run verification, confirm passing, log [PHASE 1 COMPLETE - GATE PASSED], and immediately advance to Phase 2.
```
### Phase 2: Pure Slot Generator, Padded Loader & Gate 1 Slot Tests
```
Execute Phase 2:
1. Build src/server/slot-engine/timezone-converter.ts with helpers for host wall-clock to UTC conversion and IANA timezone validation.
2. Build src/server/slot-engine/compute-slots.ts as a pure function (zero I/O, accepts explicit `now`):
- Lay out a fixed grid in host local time across the requested date window, honouring availability_schedules and date_overrides.
- Perform the DST round-trip check via formatInTimeZone to skip spring-forward gaps.
- Compute candEndUtc strictly in UTC duration (candStartUtc + duration_minutes).
- Enforce min_notice_minutes and max_horizon_days relative to `now`.
- Exclude candidates whose [candStartUtc - beforeBuffer, candEndUtc + afterBuffer) overlaps any confirmed booking's [blocked_start, blocked_end).
- Return a flat list of valid UTC slot objects: { startUtc: string; endUtc: string }[].
3. Build src/server/slot-engine/get-available-slots.ts to query Drizzle with boundary buffer padding (querying bookings padded by max buffer + 1 day on both ends of the date window).
4. Build tests/slot-engine.test.ts (Gate 1 unit tests, zero DB required):
- Standard weekday slots across Pacific, Eastern, and Tokyo timezones.
- Fixed-grid buffer collision: 10:00-10:30 booking with 10m after-buffer and 5m before-buffer excludes 10:30; next offered slot is 11:00.
- US DST spring-forward (March 9, 2025): 02:00 slot skipped; offers [01:00, 01:30, 03:00, 03:30].
- US DST fall-back (Nov 2, 2025): 01:30 candidate resolves deterministically with exact 30m physical UTC duration.
- Half-hour Asia/Kolkata timezone mapping and date override (holiday / custom hours) handling.
Run `pnpm vitest run tests/slot-engine.test.ts`. Confirm passing, log [PHASE 2 COMPLETE - GATE PASSED], and immediately advance to Phase 3.
```
### Phase 3: Atomic Booking Core, RFC 5545 `.ics` Generator, API Routes & Gate 1 `.ics` Tests
```
Execute Phase 3:
1. Build src/lib/validation.ts (Zod schemas with Intl.DateTimeFormat timezone check), src/lib/client-ip.ts (reads x-real-ip, only trusts x-forwarded-for when TRUST_PROXY=1, defaults to '127.0.0.1'), and src/lib/rate-limiter.ts (built-in in-memory sliding-window limiter allowing 5 req/min per IP by default, with optional Upstash Redis when env vars are present).
2. Build src/lib/ics-generator.ts:
- Generate RFC 5545 .ics content with UID, UTC DTSTAMP/DTSTART/DTEND, CRLF (\r\n) line endings throughout, text escaping, and 75-octet UTF-8 byte line folding.
- If rescheduled_from_id is set, preserve original booking UID and increment SEQUENCE (e.g. SEQUENCE:1).
3. Build src/server/booking/book-slot.ts (plain server module, no "use server"):
- Validate input with Zod and check rate limiter against client IP.
- Open an interactive database transaction. First execute `SELECT id FROM hosts WHERE id = $1 FOR UPDATE` to serialize concurrent booking writes for the same host and prevent 40P01 GIST deadlocks.
- If rescheduling via cancelToken, lock old booking row with `SELECT ... FOR UPDATE`, verify status === 'confirmed', and mark old row 'rescheduled'.
- Re-verify that the requested slot exists in computeSlots (excluding the old booking when rescheduling).
- Compute blocked_start and blocked_end immutably from event type buffers and insert the confirmed booking.
- Catch SQLSTATE 23P01 (exclusion_violation) and return { success: false, error: 'SLOT_TAKEN' }. If SQLSTATE 40P01 (deadlock_detected) occurs, retry up to 4 times with short backoff, then return { success: false, error: 'SLOT_TAKEN' }.
4. Build src/server/actions/book-slot-action.ts ("use server"), src/app/api/bookings/route.ts (POST handler), and src/app/api/bookings/[id]/ics/route.ts (GET handler returning text/calendar attachment).
5. Build tests/ics-generator.test.ts: verify RFC 5545 escaping, CRLF endings, UID preservation + SEQUENCE increment on reschedule, and 75-octet line folding parsed via ical.js.
Run `pnpm run test:gate1`. Confirm both slot-engine and ics-generator test suites pass, log [PHASE 3 COMPLETE - GATE PASSED], and immediately advance to Phase 4.
```
### Phase 4: Public Booking UI, Confirmation Screen & Host Availability Dashboard
```
Execute Phase 4:
1. Build components/booking/timezone-selector.tsx: accepts `timezones: string[]` prop generated on the server (`Intl.supportedValuesOf('timeZone')`) so server and browser option lists never mismatch during hydration. Detect `Intl.DateTimeFormat().resolvedOptions().timeZone` inside a `useEffect` hook after mount.
2. Build components/calendar/day-grid.tsx (groups flat UTC slots into guest local calendar dates using formatInTimeZone), components/calendar/time-slot-column.tsx (renders slot pills in guest timezone), and components/calendar/guest-details-form.tsx (name, email, notes, and submit handler wired to bookSlotAction).
3. Build components/booking/booking-confirmed-card.tsx: displays confirmed meeting details in guest timezone, cancel/reschedule token info, and a download button linking to `/api/bookings/[id]/ics`.
4. Build components/calendar/booking-shell.tsx and wire `src/app/[username]/[slug]/page.tsx` to load host, event type, and available slots, passing server-generated `timezones` to the client shell.
5. Build `src/app/dashboard/availability/page.tsx` and `src/server/actions/availability-action.ts`: allow the host to view and update weekly `availability_schedules` and `date_overrides` protected by `ADMIN_TOKEN`.
Keep all files under 200 lines. Run `pnpm run build` and `pnpm run test:gate1` to verify clean TypeScript compilation and SSR build. Log [PHASE 4 COMPLETE - GATE PASSED] and immediately advance to Phase 5.
```
### Phase 5: Gate 2 Concurrency & Boundary Integration Tests
```
Execute Phase 5:
1. Build tests/slot-loader.test.ts: verify boundary buffer padding queries against the isolated test database harness (tests/setup/pg-container.ts), confirming bookings just outside a UTC day boundary still block overlapping slots across the boundary.
2. Build tests/booking-concurrency.test.ts:
- Test 1 (Exact Slot 5-Way Race): Call bookSlot 5 times concurrently (Promise.all) for the exact same slot. Assert the race completes in under 5 seconds, exactly 1 booking succeeds with status 'confirmed', and the remaining 4 fail cleanly with { error: 'SLOT_TAKEN' } or { error: 'INVALID_SLOT' } (0 unhandled 40P01 or 23P01 exceptions).
- Test 2 (Overlapping Buffer Race): Call bookSlot concurrently for 10:00 and 10:30 when the 10:00 meeting's after-buffer overlaps the 10:30 meeting's before-buffer. Assert exactly 1 succeeds and 1 fails with SLOT_TAKEN or INVALID_SLOT.
- Test 3 (Single-Use Reschedule Token Race): Fire 2 concurrent reschedule calls using the same cancel_token. Assert 1 succeeds, 1 returns INVALID_TOKEN, and the rescheduled booking's .ics preserves the original UID with SEQUENCE:1.
3. Run the complete verification suite: `pnpm test && pnpm run build`.
Once passing with 0 errors, log [PHASE 5 COMPLETE - ALL GATES PASSED] and output instructions to run `pnpm run db:migrate && pnpm run db:seed && pnpm run dev` and open http://localhost:3000/demo-host/30-min-intro.
```
End of specification • 326 lines