Intermediate ~4 hours

Build an AI Booking Assistant

Create a conversational AI chatbot that handles hotel and restaurant bookings naturally. Guests can check availability, make reservations, and receive confirmations - all through a friendly chat interface powered by Claude.

What We're Building

We're building an AI-powered booking assistant that can handle natural language conversations with guests. Instead of filling out forms, users simply chat with the assistant to make reservations, check availability, modify bookings, or ask questions about the venue.

U

Hi! I'd like to book a table for 4 this Saturday evening around 7pm

A

I'd be happy to help you book a table for 4 this Saturday! Let me check availability for the evening of January 18th around 7pm... I have openings at 6:30pm, 7:00pm, and 7:45pm. Which time works best for you?

U

7pm is perfect!

A

Excellent! I've reserved a table for 4 at 7:00pm on Saturday, January 18th. Can I have your name and phone number to complete the booking? We'll send you a confirmation text.

The assistant handles the entire booking flow conversationally, including:

  • Real-time availability checking against your database
  • Natural date/time parsing ("next Friday", "7ish", "evening")
  • Guest information collection with validation
  • Booking confirmation with email/SMS notifications
  • Modification and cancellation handling

Prerequisites

Before starting this guide, make sure you have the following:

  • Node.js 18+ installed on your machine
  • A Supabase account (free tier works)
  • An Anthropic API key for Claude
  • Basic familiarity with React and TypeScript
  • A Vercel account for deployment

Tech Stack Specification

Here's the technology stack we'll use for this build:

Layer Technology Why This Choice
Frontend Next.js 14 Server components + streaming chat support
Backend Vercel AI SDK Built-in streaming, tool calling, easy Claude integration
Database Supabase Postgres with real-time subscriptions for booking storage
AI Claude API Natural conversations, excellent at understanding context
Calendar Cal.com or custom Flexible scheduling with availability rules
Hosting Vercel Edge runtime support, seamless Next.js deployment

AI Agent Workflow

Here's how to leverage AI tools throughout this build to maximize productivity:

Project Scaffolding with Claude Code

Start by using Claude Code to scaffold the entire project structure. This saves hours of boilerplate setup and ensures best practices from the start.

Claude Code prompt
# Create a Next.js 14 booking assistant with:
# - App router with streaming chat page
# - Vercel AI SDK integration with Claude
# - Supabase client setup with booking tables
# - Tool definitions for check_availability and create_booking
# - TypeScript types for Booking, TimeSlot, Guest

npx create-next-app@latest booking-assistant --typescript --tailwind --app
cd booking-assistant
npm install ai @anthropic-ai/sdk @supabase/supabase-js

UI Generation with v0.dev

Use v0.dev to rapidly generate the chat interface components. Ask for a modern chat UI with message bubbles, typing indicators, and a sleek input area.

Pro Tip

When prompting v0.dev, specify "hospitality" or "hotel" in your prompt to get industry-appropriate styling with warm colors and professional aesthetics.

Development with Cursor

Use Cursor's AI features to iterate quickly on the booking logic. The inline completions are particularly helpful when writing Supabase queries and Claude tool definitions.

Step-by-Step Build Guide

Phase 1: Chat Interface Setup with Streaming

First, we'll create the streaming chat interface. The Vercel AI SDK makes this remarkably simple with the useChat hook.

app/page.tsx
'use client';

import { useChat } from 'ai/react';
import { useState } from 'react';

export default function BookingChat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api: '/api/chat',
      initialMessages: [{
        id: 'welcome',
        role: 'assistant',
        content: `Hi! I'm your booking assistant. I can help you
          make a reservation, check availability, or modify
          an existing booking. How can I help you today?`
      }]
    });

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto">
      <header className="p-4 border-b">
        <h1 className="text-xl font-semibold">Booking Assistant</h1>
      </header>

      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[80%] rounded-2xl px-4 py-2 ${
                message.role === 'user'
                  ? 'bg-orange-500 text-white'
                  : 'bg-gray-100 text-gray-900'
              }`}
            >
              {message.content}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-gray-100 rounded-2xl px-4 py-2">
              <span className="animate-pulse">Thinking...</span>
            </div>
          </div>
        )}
      </div>

      <form onSubmit={handleSubmit} className="p-4 border-t">
        <div className="flex gap-2">
          <input
            value={input}
            onChange={handleInputChange}
            placeholder="Type your message..."
            className="flex-1 rounded-full px-4 py-2 border focus:outline-none focus:ring-2 focus:ring-orange-500"
          />
          <button
            type="submit"
            disabled={isLoading}
            className="bg-orange-500 text-white rounded-full px-6 py-2 hover:bg-orange-600 disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

Phase 2: Booking Database Schema

Set up your Supabase database with tables for bookings, time slots, and guests. This schema supports both restaurant tables and hotel rooms.

supabase/migrations/001_bookings.sql
-- Bookable resources (tables, rooms, etc.)
CREATE TABLE resources (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  type TEXT NOT NULL, -- 'table', 'room', etc.
  capacity INTEGER NOT NULL,
  metadata JSONB DEFAULT '{}'
);

-- Time slots for availability
CREATE TABLE time_slots (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  resource_id UUID REFERENCES resources(id),
  start_time TIMESTAMPTZ NOT NULL,
  end_time TIMESTAMPTZ NOT NULL,
  is_available BOOLEAN DEFAULT true
);

-- Guest information
CREATE TABLE guests (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  email TEXT,
  phone TEXT,
  notes TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Bookings
CREATE TABLE bookings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  guest_id UUID REFERENCES guests(id),
  resource_id UUID REFERENCES resources(id),
  time_slot_id UUID REFERENCES time_slots(id),
  party_size INTEGER NOT NULL,
  status TEXT DEFAULT 'confirmed',
  special_requests TEXT,
  confirmation_code TEXT UNIQUE,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Index for fast availability queries
CREATE INDEX idx_time_slots_availability
ON time_slots(start_time, is_available)
WHERE is_available = true;

Phase 3: Availability Checking Logic

Create a robust availability checking system that the AI can call as a tool. This function handles date parsing and returns available time slots.

lib/availability.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

export interface AvailabilityQuery {
  date: string;      // ISO date string
  partySize: number;
  preferredTime?: string; // 'morning', 'afternoon', 'evening', or HH:MM
}

export interface TimeSlotResult {
  id: string;
  startTime: string;
  endTime: string;
  resourceName: string;
  resourceId: string;
}

export async function checkAvailability(
  query: AvailabilityQuery
): Promise<TimeSlotResult[]> {
  const { date, partySize, preferredTime } = query;

  // Build time range based on preference
  let startHour = 11;
  let endHour = 22;

  if (preferredTime === 'morning') {
    startHour = 11; endHour = 14;
  } else if (preferredTime === 'afternoon') {
    startHour = 14; endHour = 17;
  } else if (preferredTime === 'evening') {
    startHour = 17; endHour = 22;
  } else if (preferredTime) {
    // Specific time - search ±2 hours
    const [hours] = preferredTime.split(':').map(Number);
    startHour = Math.max(11, hours - 2);
    endHour = Math.min(22, hours + 2);
  }

  const startDate = new Date(`${date}T${startHour}:00:00`);
  const endDate = new Date(`${date}T${endHour}:00:00`);

  // Query available slots with capacity
  const { data, error } = await supabase
    .from('time_slots')
    .select(`
      id,
      start_time,
      end_time,
      resources!inner (
        id,
        name,
        capacity
      )
    `)
    .eq('is_available', true)
    .gte('start_time', startDate.toISOString())
    .lte('start_time', endDate.toISOString())
    .gte('resources.capacity', partySize)
    .order('start_time');

  if (error) throw error;

  return data.map(slot => ({
    id: slot.id,
    startTime: slot.start_time,
    endTime: slot.end_time,
    resourceName: slot.resources.name,
    resourceId: slot.resources.id
  }));
}

Phase 4: Claude Integration for Natural Language Booking

Now we wire up Claude with tool calling capabilities. The AI can check availability and create bookings by calling our functions.

app/api/chat/route.ts
import { anthropic } from '@ai-sdk/anthropic';
import { streamText, tool } from 'ai';
import { z } from 'zod';
import { checkAvailability } from '@/lib/availability';
import { createBooking } from '@/lib/bookings';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: anthropic('claude-sonnet-4-20250514'),
    system: `You are a friendly booking assistant for The Grand Hotel
& Restaurant. Help guests make reservations conversationally.

When checking availability:
- Parse natural language dates ("next Saturday", "tomorrow")
- Understand time preferences ("evening", "around 7pm", "dinner time")
- Ask for party size if not provided

When making bookings:
- Collect guest name and phone number
- Confirm all details before booking
- Provide the confirmation code after booking

Be warm, professional, and efficient. Use a conversational tone.`,
    messages,
    tools: {
      checkAvailability: tool({
        description: 'Check available time slots for a booking',
        parameters: z.object({
          date: z.string().describe('ISO date (YYYY-MM-DD)'),
          partySize: z.number().describe('Number of guests'),
          preferredTime: z.string().optional()
            .describe('morning, afternoon, evening, or HH:MM')
        }),
        execute: async ({ date, partySize, preferredTime }) => {
          const slots = await checkAvailability({
            date, partySize, preferredTime
          });
          return {
            availableSlots: slots,
            message: slots.length > 0
              ? `Found ${slots.length} available times`
              : 'No availability for those criteria'
          };
        }
      }),

      createBooking: tool({
        description: 'Create a new booking reservation',
        parameters: z.object({
          timeSlotId: z.string().describe('ID of selected time slot'),
          guestName: z.string().describe('Guest full name'),
          guestPhone: z.string().describe('Phone number'),
          guestEmail: z.string().optional().describe('Email address'),
          partySize: z.number().describe('Number of guests'),
          specialRequests: z.string().optional()
        }),
        execute: async (params) => {
          const booking = await createBooking(params);
          return {
            success: true,
            confirmationCode: booking.confirmationCode,
            bookingDetails: booking
          };
        }
      })
    },
    maxSteps: 5, // Allow multi-step conversations
  });

  return result.toDataStreamResponse();
}

Phase 5: Confirmation and Notification System

Set up automatic confirmations via email and SMS when bookings are created or modified.

lib/notifications.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export interface BookingConfirmation {
  guestName: string;
  guestEmail: string;
  guestPhone: string;
  confirmationCode: string;
  dateTime: string;
  partySize: number;
  venueName: string;
}

export async function sendBookingConfirmation(
  booking: BookingConfirmation
) {
  const { guestName, guestEmail, confirmationCode, dateTime, partySize } = booking;

  // Send email confirmation
  await resend.emails.send({
    from: 'bookings@thegrandhotel.com',
    to: guestEmail,
    subject: `Booking Confirmed - ${confirmationCode}`,
    html: `
      <h1>Your Reservation is Confirmed!</h1>
      <p>Dear ${guestName},</p>
      <p>We're delighted to confirm your reservation:</p>
      <ul>
        <li><strong>Date & Time:</strong> ${formatDateTime(dateTime)}</li>
        <li><strong>Party Size:</strong> ${partySize} guests</li>
        <li><strong>Confirmation Code:</strong> ${confirmationCode}</li>
      </ul>
      <p>To modify or cancel, reply to this email or chat with
      our booking assistant.</p>
      <p>We look forward to seeing you!</p>
    `
  });

  // Optional: Send SMS via Twilio
  if (process.env.TWILIO_ENABLED) {
    await sendSMS(booking.guestPhone,
      `Booking confirmed at The Grand! ${formatDateTime(dateTime)} ` +
      `for ${partySize}. Code: ${confirmationCode}`
    );
  }
}

function formatDateTime(isoString: string): string {
  return new Date(isoString).toLocaleString('en-US', {
    weekday: 'long',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit'
  });
}

Phase 6: Admin Dashboard for Managing Bookings

Create a simple admin dashboard to view, modify, and manage all bookings. This uses Supabase's real-time subscriptions for live updates.

app/admin/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export default function AdminDashboard() {
  const [bookings, setBookings] = useState([]);
  const [filter, setFilter] = useState('today');

  useEffect(() => {
    // Initial fetch
    fetchBookings();

    // Real-time subscription
    const subscription = supabase
      .channel('bookings')
      .on('postgres_changes', {
        event: '*',
        schema: 'public',
        table: 'bookings'
      }, () => fetchBookings())
      .subscribe();

    return () => subscription.unsubscribe();
  }, [filter]);

  async function fetchBookings() {
    const today = new Date().toISOString().split('T')[0];

    const { data } = await supabase
      .from('bookings')
      .select(`
        *,
        guests (name, phone, email),
        time_slots (start_time, end_time),
        resources (name)
      `)
      .gte('time_slots.start_time', `${today}T00:00:00`)
      .order('time_slots.start_time');

    setBookings(data || []);
  }

  return (
    <div className="p-8 max-w-6xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">Booking Dashboard</h1>

      <div className="grid gap-4">
        {bookings.map((booking) => (
          <div
            key={booking.id}
            className="p-4 border rounded-lg flex justify-between items-center"
          >
            <div>
              <p className="font-semibold">{booking.guests.name}</p>
              <p className="text-sm text-gray-600">
                {formatTime(booking.time_slots.start_time)} -
                {booking.resources.name} -
                {booking.party_size} guests
              </p>
            </div>
            <div className="flex gap-2">
              <span className={`px-3 py-1 rounded-full text-sm ${
                booking.status === 'confirmed'
                  ? 'bg-green-100 text-green-800'
                  : 'bg-yellow-100 text-yellow-800'
              }`}>
                {booking.status}
              </span>
              <span className="text-sm text-gray-500">
                {booking.confirmation_code}
              </span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Common Issues & Solutions

Here are some common issues you might encounter and how to solve them:

Streaming Not Working

If messages appear all at once instead of streaming, ensure you're using toDataStreamResponse() and that your client uses the useChat hook from ai/react.

Date Parsing Errors

Claude may sometimes misinterpret dates. Add explicit validation in your checkAvailability tool and handle edge cases like "next week" or ambiguous dates by asking for clarification.

Double Bookings

Use database transactions when creating bookings. In Supabase, wrap your insert with a check for availability using rpc functions to ensure atomicity.

Next Steps

Congratulations on building your AI booking assistant! Here are some ways to extend it:

  • Add multi-language support - Claude handles multiple languages naturally; just update your system prompt to respond in the guest's language
  • Integrate with calendar apps - Use Cal.com's API or Google Calendar to sync bookings with staff schedules
  • Add payment processing - Require deposits for peak times using Stripe's Payment Links
  • Build a waitlist system - When fully booked, offer to add guests to a waitlist with automatic notifications
  • Add analytics - Track booking patterns, peak times, and no-show rates to optimize operations