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.
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.
# 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.
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.
'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.
-- 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.
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.
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.
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.
'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:
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.
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.
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
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.