[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
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.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.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.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
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

Related use cases