[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Calendar Booking System

Build a Calendar Booking System

Build a Calendly-style booking system with availability management, timezone handling, buffer times, Google Calendar sync, and automated reminders — letting clients self-schedule without back-and-forth emails.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Alex runs a 15-person consulting firm. Scheduling meetings takes 4-5 emails per booking — "Are you free Tuesday?", "How about 3pm?", "Sorry, that's taken now." Clients in different timezones get confused about meeting times and miss calls. Double-bookings happen because Google Calendar isn't checked before confirming. They're paying $1,200/year for Calendly but need custom features: team round-robin, intake forms, and integration with their CRM. They need a self-hosted booking system.

Step 1: Build the Booking Engine

typescript
// src/booking/engine.ts — Calendar booking with availability, timezone, and Google Calendar sync
import { pool } from "../db";
import { Redis } from "ioredis";
import { z } from "zod";

const redis = new Redis(process.env.REDIS_URL!);

interface AvailabilityRule {
  dayOfWeek: number;           // 0=Sunday, 6=Saturday
  startTime: string;           // "09:00" in owner's timezone
  endTime: string;             // "17:00"
  enabled: boolean;
}

interface EventType {
  id: string;
  slug: string;
  name: string;
  duration: number;            // minutes
  bufferBefore: number;        // minutes between meetings
  bufferAfter: number;
  maxPerDay: number;           // max bookings per day
  requiresApproval: boolean;
  questions: Array<{ label: string; required: boolean; type: "text" | "textarea" | "select" }>;
}

interface TimeSlot {
  start: string;               // ISO datetime in UTC
  end: string;
  available: boolean;
}

// Get available slots for a date range
export async function getAvailableSlots(
  eventTypeId: string,
  hostId: string,
  dateFrom: string,             // YYYY-MM-DD
  dateTo: string,
  clientTimezone: string        // e.g. "America/New_York"
): Promise<TimeSlot[]> {
  const eventType = await getEventType(eventTypeId);
  if (!eventType) throw new Error("Event type not found");

  const { rows: [host] } = await pool.query(
    "SELECT timezone, availability_rules FROM users WHERE id = $1",
    [hostId]
  );

  const rules: AvailabilityRule[] = host.availability_rules || getDefaultRules();
  const hostTz = host.timezone || "UTC";

  // Get existing bookings in the range
  const { rows: bookings } = await pool.query(
    `SELECT start_time, end_time FROM bookings
     WHERE host_id = $1 AND status != 'cancelled'
     AND start_time >= $2::date AND end_time <= ($3::date + interval '1 day')`,
    [hostId, dateFrom, dateTo]
  );

  // Get Google Calendar busy times
  const gcalBusy = await getGoogleCalendarBusy(hostId, dateFrom, dateTo);
  const allBusy = [
    ...bookings.map((b) => ({ start: new Date(b.start_time), end: new Date(b.end_time) })),
    ...gcalBusy,
  ];

  // Generate slots day by day
  const slots: TimeSlot[] = [];
  const start = new Date(dateFrom);
  const end = new Date(dateTo);

  for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
    const dayOfWeek = d.getDay();
    const rule = rules.find((r) => r.dayOfWeek === dayOfWeek && r.enabled);
    if (!rule) continue;

    // Count bookings on this day
    const dayStr = d.toISOString().slice(0, 10);
    const dayBookings = bookings.filter((b) =>
      new Date(b.start_time).toISOString().slice(0, 10) === dayStr
    );
    if (dayBookings.length >= eventType.maxPerDay) continue;

    // Generate time slots
    const daySlots = generateDaySlots(
      d, rule, eventType, allBusy, hostTz
    );
    slots.push(...daySlots);
  }

  // Filter out past slots
  const now = new Date();
  const minimumNotice = 2 * 3600000; // 2 hours minimum notice
  return slots.filter((s) => new Date(s.start).getTime() > now.getTime() + minimumNotice);
}

function generateDaySlots(
  date: Date,
  rule: AvailabilityRule,
  eventType: EventType,
  busy: Array<{ start: Date; end: Date }>,
  hostTz: string
): TimeSlot[] {
  const slots: TimeSlot[] = [];
  const dateStr = date.toISOString().slice(0, 10);

  // Parse start/end times in host timezone
  const dayStart = new Date(`${dateStr}T${rule.startTime}:00`);
  const dayEnd = new Date(`${dateStr}T${rule.endTime}:00`);

  const slotDuration = eventType.duration * 60000;
  const bufferBefore = eventType.bufferBefore * 60000;
  const bufferAfter = eventType.bufferAfter * 60000;
  const step = 15 * 60000; // 15-min increments

  for (
    let slotStart = dayStart.getTime();
    slotStart + slotDuration <= dayEnd.getTime();
    slotStart += step
  ) {
    const slotEnd = slotStart + slotDuration;
    const blockStart = slotStart - bufferBefore;
    const blockEnd = slotEnd + bufferAfter;

    // Check conflicts with busy times
    const hasConflict = busy.some((b) =>
      blockStart < b.end.getTime() && blockEnd > b.start.getTime()
    );

    slots.push({
      start: new Date(slotStart).toISOString(),
      end: new Date(slotEnd).toISOString(),
      available: !hasConflict,
    });
  }

  return slots.filter((s) => s.available);
}

// Create booking
export async function createBooking(
  eventTypeId: string,
  hostId: string,
  clientEmail: string,
  clientName: string,
  startTime: string,
  clientTimezone: string,
  answers?: Record<string, string>
): Promise<{ bookingId: string; confirmed: boolean; meetingUrl?: string }> {
  const eventType = await getEventType(eventTypeId);
  if (!eventType) throw new Error("Event type not found");

  const start = new Date(startTime);
  const end = new Date(start.getTime() + eventType.duration * 60000);

  // Lock slot to prevent double-booking
  const lockKey = `booking:lock:${hostId}:${start.toISOString()}`;
  const locked = await redis.set(lockKey, "1", "EX", 300, "NX");
  if (!locked) throw new Error("This time slot was just booked. Please choose another.");

  try {
    // Verify slot is still available
    const { rows: conflicts } = await pool.query(
      `SELECT 1 FROM bookings
       WHERE host_id = $1 AND status != 'cancelled'
       AND start_time < $3 AND end_time > $2`,
      [hostId, start, end]
    );
    if (conflicts.length > 0) throw new Error("Slot no longer available");

    const bookingId = `bk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const confirmed = !eventType.requiresApproval;

    await pool.query(
      `INSERT INTO bookings (id, event_type_id, host_id, client_email, client_name,
         start_time, end_time, client_timezone, answers, status, created_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())`,
      [bookingId, eventTypeId, hostId, clientEmail, clientName,
       start, end, clientTimezone, JSON.stringify(answers || {}),
       confirmed ? "confirmed" : "pending"]
    );

    if (confirmed) {
      // Create Google Calendar event
      const meetingUrl = await createGoogleCalendarEvent(hostId, {
        summary: `${eventType.name} with ${clientName}`,
        start, end,
        attendeeEmail: clientEmail,
      });

      // Schedule reminder emails
      await scheduleReminders(bookingId, start, clientEmail, hostId);

      return { bookingId, confirmed: true, meetingUrl };
    }

    // Notify host for approval
    await redis.rpush("notification:queue", JSON.stringify({
      userId: hostId, type: "booking_pending", data: { bookingId, clientName, startTime },
    }));

    return { bookingId, confirmed: false };
  } finally {
    await redis.del(lockKey);
  }
}

// Schedule reminder emails (24h and 1h before)
async function scheduleReminders(bookingId: string, startTime: Date, clientEmail: string, hostId: string): Promise<void> {
  const reminders = [
    { offset: 24 * 3600000, label: "24 hours" },
    { offset: 1 * 3600000, label: "1 hour" },
  ];

  for (const reminder of reminders) {
    const sendAt = startTime.getTime() - reminder.offset;
    if (sendAt > Date.now()) {
      await redis.zadd("reminder:queue", sendAt, JSON.stringify({
        bookingId, clientEmail, hostId, label: reminder.label,
      }));
    }
  }
}

function getDefaultRules(): AvailabilityRule[] {
  return [1, 2, 3, 4, 5].map((day) => ({
    dayOfWeek: day,
    startTime: "09:00",
    endTime: "17:00",
    enabled: true,
  }));
}

async function getEventType(id: string): Promise<EventType | null> {
  const { rows } = await pool.query("SELECT * FROM event_types WHERE id = $1", [id]);
  return rows[0] || null;
}

async function getGoogleCalendarBusy(hostId: string, from: string, to: string): Promise<Array<{ start: Date; end: Date }>> {
  // Google Calendar API freebusy query
  return [];
}

async function createGoogleCalendarEvent(hostId: string, event: any): Promise<string> {
  // Create event via Google Calendar API, return Meet link
  return "https://meet.google.com/abc-defg-hij";
}

Results

  • Scheduling emails: 5 per booking → 0 — clients pick a time from available slots; no back-and-forth; confirmed instantly
  • Double-bookings eliminated — Redis lock + DB conflict check + Google Calendar sync ensures no overlapping meetings
  • Timezone confusion gone — slots shown in client's local timezone; confirmation email includes both timezones; no more "was that 3pm your time or mine?"
  • No-show rate: 20% → 5% — automated reminders at 24h and 1h before with a reschedule link; Google Calendar event with Meet link added automatically
  • $1,200/year Calendly cost eliminated — self-hosted booking system with custom features; CRM integration built exactly to their needs