Build a service booking platform with V0 using Next.js, Supabase, and Stripe. You'll get a public booking flow with availability calendar, time slot picker, payment collection, automated email confirmations, and a provider management dashboard — all in about 1-2 hours.
What you're building
Service businesses like salons, consultants, and clinics lose revenue when booking is friction-filled. Customers want to see availability, pick a time, pay, and get a confirmation — all in under a minute. Manual scheduling via phone or email cannot compete.
V0 generates the booking flow, provider dashboard, and payment integration from prompts. Supabase via the Connect panel handles availability rules, booking records, and customer data. Stripe via Vercel Marketplace processes payments at booking time.
The architecture uses Next.js Server Components for the public booking pages (service listing, calendar, time slots), a Supabase RPC function with an exclusion constraint to prevent double-bookings at the database level, a Stripe Checkout route for payment, and a webhook handler to confirm bookings after payment succeeds. Server Actions handle status updates and cancellations.
Final result
A complete booking platform where customers browse services, pick available time slots, pay via Stripe, and receive email confirmation — with a provider dashboard for managing availability and appointments.
Tech stack
Prerequisites
- A V0 account (Premium plan recommended for multi-page builds)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Stripe account (test mode works for development)
- A Resend account for email confirmations (free tier: 3,000 emails/month)
Build steps
Set up the database schema with availability and booking tables
Create a new V0 project, connect Supabase and Stripe via the Connect panel. Create the providers, services, availability, and bookings tables with an exclusion constraint that prevents overlapping bookings for the same provider.
1// Paste this prompt into V0's AI chat:2// Build a booking platform with Supabase. Create these tables:3// 1. providers: id (uuid PK), user_id (uuid FK to auth.users), name (text), bio (text), avatar_url (text), timezone (text default 'UTC')4// 2. services: id (uuid PK), provider_id (uuid FK), name (text), duration_minutes (int), price (numeric), description (text), is_active (boolean default true)5// 3. availability: id (uuid PK), provider_id (uuid FK), day_of_week (int 0-6), start_time (time), end_time (time)6// 4. bookings: id (uuid PK), service_id (uuid FK), provider_id (uuid FK), customer_id (uuid FK), starts_at (timestamptz), ends_at (timestamptz), status (text CHECK in 'pending','confirmed','cancelled','completed'), notes (text), stripe_payment_intent_id (text), created_at (timestamptz)7// Add exclusion constraint on bookings: EXCLUDE USING gist (provider_id WITH =, tstzrange(starts_at, ends_at) WITH &&) WHERE (status != 'cancelled')8// Add RLS so customers see only their own bookings, providers see their own.Pro tip: The GiST exclusion constraint is the key to preventing double-bookings. It ensures that no two non-cancelled bookings for the same provider can have overlapping time ranges — enforced at the database level.
Expected result: Supabase and Stripe are connected. Tables are created with the exclusion constraint preventing overlapping bookings per provider.
Build the public booking flow with service and time slot selection
Create the customer-facing booking pages. Customers first select a provider and service, then pick a date from a Calendar, and finally choose an available time slot. Available slots are calculated by checking the provider's weekly availability against existing bookings.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function GET(req: NextRequest) {10 const { searchParams } = new URL(req.url)11 const providerId = searchParams.get('provider_id')!12 const serviceId = searchParams.get('service_id')!13 const date = searchParams.get('date')!1415 const dayOfWeek = new Date(date).getDay()1617 const { data: availability } = await supabase18 .from('availability')19 .select('start_time, end_time')20 .eq('provider_id', providerId)21 .eq('day_of_week', dayOfWeek)2223 const { data: service } = await supabase24 .from('services')25 .select('duration_minutes')26 .eq('id', serviceId)27 .single()2829 if (!availability?.length || !service) {30 return NextResponse.json({ slots: [] })31 }3233 const { data: existingBookings } = await supabase34 .from('bookings')35 .select('starts_at, ends_at')36 .eq('provider_id', providerId)37 .neq('status', 'cancelled')38 .gte('starts_at', `${date}T00:00:00`)39 .lte('starts_at', `${date}T23:59:59`)4041 const slots: string[] = []42 for (const block of availability) {43 let current = new Date(`${date}T${block.start_time}`)44 const end = new Date(`${date}T${block.end_time}`)4546 while (current.getTime() + service.duration_minutes * 60000 <= end.getTime()) {47 const slotEnd = new Date(current.getTime() + service.duration_minutes * 60000)48 const conflicts = existingBookings?.some((b) => {49 const bStart = new Date(b.starts_at).getTime()50 const bEnd = new Date(b.ends_at).getTime()51 return current.getTime() < bEnd && slotEnd.getTime() > bStart52 })53 if (!conflicts) slots.push(current.toISOString())54 current = new Date(current.getTime() + 30 * 60000)55 }56 }5758 return NextResponse.json({ slots })59}Expected result: The API returns available time slots for a given provider, service, and date by cross-referencing weekly availability with existing bookings.
Create the booking confirmation with Stripe payment
When a customer selects a time slot, create a Stripe Checkout Session that includes the service price and booking details. After payment, the Stripe webhook confirms the booking and sends a confirmation email.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'34const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)56export async function POST(req: NextRequest) {7 const { serviceId, providerId, customerId, startsAt, endsAt, serviceName, price } = await req.json()89 const session = await stripe.checkout.sessions.create({10 mode: 'payment',11 payment_method_types: ['card'],12 line_items: [{13 price_data: {14 currency: 'usd',15 product_data: { name: serviceName },16 unit_amount: Math.round(price * 100),17 },18 quantity: 1,19 }],20 success_url: `${req.nextUrl.origin}/bookings?success=true`,21 cancel_url: `${req.nextUrl.origin}/book/${providerId}`,22 metadata: {23 service_id: serviceId,24 provider_id: providerId,25 customer_id: customerId,26 starts_at: startsAt,27 ends_at: endsAt,28 },29 })3031 return NextResponse.json({ url: session.url })32}Pro tip: Use prompt queuing in V0 — queue the booking flow UI, the slot calculation API, and the Stripe checkout separately so V0 generates each feature cleanly without context confusion.
Expected result: Selecting a time slot creates a Stripe Checkout Session. After payment, the customer is redirected to a success page.
Build the webhook handler and email confirmation
Create the Stripe webhook handler that confirms bookings after payment and sends email confirmations to both the customer and provider using Resend.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'4import { Resend } from 'resend'56const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)7const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)8const resend = new Resend(process.env.RESEND_API_KEY)910export async function POST(req: NextRequest) {11 const body = await req.text()12 const sig = req.headers.get('stripe-signature')!1314 let event: Stripe.Event15 try {16 event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)17 } catch {18 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })19 }2021 if (event.type === 'checkout.session.completed') {22 const session = event.data.object as Stripe.Checkout.Session23 const meta = session.metadata!2425 const { data: booking, error } = await supabase.from('bookings').insert({26 service_id: meta.service_id,27 provider_id: meta.provider_id,28 customer_id: meta.customer_id,29 starts_at: meta.starts_at,30 ends_at: meta.ends_at,31 status: 'confirmed',32 stripe_payment_intent_id: session.payment_intent as string,33 }).select().single()3435 if (!error && session.customer_details?.email) {36 await resend.emails.send({37 from: 'bookings@yourdomain.com',38 to: session.customer_details.email,39 subject: 'Booking Confirmed',40 text: `Your booking is confirmed for ${new Date(meta.starts_at).toLocaleString()}.`,41 })42 }43 }4445 return NextResponse.json({ received: true })46}Expected result: After successful payment, a booking record is created in Supabase with status confirmed, and the customer receives a confirmation email.
Build the provider dashboard for managing bookings and availability
Create the provider-facing dashboard where they can set their weekly availability, view upcoming bookings, and update booking status (confirm, complete, cancel). Use Server Components for data fetching.
1// Paste this prompt into V0's AI chat:2// Build a provider dashboard at app/dashboard/bookings/page.tsx.3// Requirements:4// - Calendar view showing upcoming bookings as colored blocks5// - Table view with columns: Date, Time, Customer, Service, Status Badge, Actions6// - Tabs to switch between Calendar and Table views7// - Availability management page with day-of-week Select, start/end time pickers for each day8// - Server Actions for updating booking status (confirm, complete, cancel)9// - Cancel action should show AlertDialog with confirmation10// - Badge colors: pending=yellow, confirmed=green, cancelled=red, completed=blue11// - Use Server Components for initial data fetch from SupabaseExpected result: The provider dashboard shows upcoming bookings in both calendar and table views. Providers can manage their weekly availability and update booking statuses.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function GET(req: NextRequest) {10 const { searchParams } = new URL(req.url)11 const providerId = searchParams.get('provider_id')!12 const serviceId = searchParams.get('service_id')!13 const date = searchParams.get('date')!1415 const dayOfWeek = new Date(date).getDay()1617 const [{ data: availability }, { data: service }, { data: bookings }] =18 await Promise.all([19 supabase20 .from('availability')21 .select('start_time, end_time')22 .eq('provider_id', providerId)23 .eq('day_of_week', dayOfWeek),24 supabase25 .from('services')26 .select('duration_minutes')27 .eq('id', serviceId)28 .single(),29 supabase30 .from('bookings')31 .select('starts_at, ends_at')32 .eq('provider_id', providerId)33 .neq('status', 'cancelled')34 .gte('starts_at', `${date}T00:00:00`)35 .lte('starts_at', `${date}T23:59:59`),36 ])3738 if (!availability?.length || !service) {39 return NextResponse.json({ slots: [] })40 }4142 const slots: string[] = []43 const duration = service.duration_minutes * 600004445 for (const block of availability) {46 let current = new Date(`${date}T${block.start_time}`)47 const end = new Date(`${date}T${block.end_time}`)4849 while (current.getTime() + duration <= end.getTime()) {50 const slotEnd = new Date(current.getTime() + duration)51 const hasConflict = bookings?.some((b) => {52 const bStart = new Date(b.starts_at).getTime()53 const bEnd = new Date(b.ends_at).getTime()54 return current.getTime() < bEnd && slotEnd.getTime() > bStart55 })56 if (!hasConflict) slots.push(current.toISOString())57 current = new Date(current.getTime() + 30 * 60000)58 }59 }6061 return NextResponse.json({ slots })62}Customization ideas
Add recurring bookings
Let customers book a recurring weekly appointment by generating multiple booking records at checkout time, with a Server Action that creates bookings for the next N weeks.
Add calendar sync
Generate .ics calendar files for each booking and attach them to the confirmation email so customers can add appointments to Google Calendar or Apple Calendar.
Add waitlist for full slots
When a time slot is fully booked, show a Join Waitlist button. If the original booking is cancelled, automatically notify and offer the slot to waitlisted customers.
Add buffer time between appointments
Add a configurable buffer_minutes field to services that adds padding between appointments, preventing back-to-back bookings with no transition time.
Common pitfalls
Pitfall: Checking for availability only in application code
How to avoid: Use a PostgreSQL exclusion constraint: EXCLUDE USING gist (provider_id WITH =, tstzrange(starts_at, ends_at) WITH &&). This prevents overlapping bookings at the database level regardless of application timing.
Pitfall: Creating the booking before payment is confirmed
How to avoid: Create the booking only in the Stripe webhook after payment is confirmed. Pass all booking details in the Checkout Session metadata so the webhook has everything it needs.
Pitfall: Not handling timezone differences between providers and customers
How to avoid: Store the provider's timezone in the providers table. Convert availability times to UTC for database operations, and display them in the customer's local timezone on the frontend using Intl.DateTimeFormat.
Best practices
- Use PostgreSQL exclusion constraints to prevent double-bookings at the database level, not just in application code
- Create bookings only after Stripe webhook confirms payment to avoid blocking slots for abandoned checkouts
- Store RESEND_API_KEY and STRIPE_WEBHOOK_SECRET in V0's Vars tab without NEXT_PUBLIC_ prefix
- Use Server Components for booking pages and the provider dashboard to keep database queries server-side
- Use prompt queuing in V0 — queue the booking flow, slot API, checkout integration, and dashboard separately
- Use Design Mode (Option+D) to adjust the time slot grid layout, service card styling, and calendar appearance without credits
- Always store and compare times in UTC using timestamptz columns, displaying in local timezone only on the frontend
- Add email confirmations for bookings, cancellations, and reminders to reduce no-shows
AI prompts to try
Copy these prompts to build this project faster.
I'm building a service booking platform with Next.js App Router, Supabase, and Stripe. I need availability management, slot calculation that accounts for existing bookings, a PostgreSQL exclusion constraint to prevent double-bookings, and payment-confirmed booking creation via Stripe webhooks. Help me design the slot calculation algorithm.
Build an available time slot calculator API that takes provider_id, service_id, and date as parameters. It should: fetch the provider's weekly availability for that day of week, fetch the service duration, fetch existing non-cancelled bookings for that date, then generate 30-minute-interval slots that fit within availability windows and do not overlap with existing bookings. Return the available slots as ISO timestamp strings.
Frequently asked questions
How does the double-booking prevention work?
A PostgreSQL exclusion constraint on the bookings table prevents overlapping time ranges for the same provider. If two customers try to book the same slot simultaneously, the database rejects the second booking regardless of application-level timing. This is more reliable than application-level checks.
What happens if a customer abandons payment?
The booking is only created after the Stripe webhook confirms payment. If the customer abandons the Checkout page, no booking is created and the slot remains available for other customers.
What V0 plan do I need for a booking platform?
V0 Premium is recommended because the platform requires multiple pages (booking flow, provider dashboard), API routes (slots, checkout, webhook), and integrations with Supabase, Stripe, and Resend.
How do I handle cancellations and refunds?
Create a Server Action that updates the booking status to cancelled and calls the Stripe Refund API to issue a refund. The cancellation releases the time slot (the exclusion constraint only blocks non-cancelled bookings) so it becomes available again.
How do I deploy the booking platform?
Click Share then Publish to Production in V0. After publishing, register the Stripe webhook URL and update the Resend from address to match your production domain.
Can RapidDev help build a custom booking platform?
Yes. RapidDev has built 600+ apps including complex booking systems with multi-location support, resource management, and calendar integrations. Book a free consultation to discuss your specific booking requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation