Build a Calendly-style scheduling app with V0 using Next.js and Supabase that lets professionals set weekly availability, create custom event types, accept bookings from a public page, and sync with Google Calendar — all in about 1-2 hours without local setup.
What you're building
Scheduling tools like Calendly solve a real friction point — the back-and-forth of finding a meeting time. A professional sets their availability, shares a booking link, and clients pick an open slot. The meeting appears on both calendars automatically.
V0 generates the booking page, availability editor, event type manager, and dashboard from prompts. The key technical challenge is computing available slots accurately. A server-side function generates all possible slots from the host's weekly availability, fetches existing bookings for the selected date range, subtracts conflicts including buffer minutes before and after each booking, and converts everything to the guest's timezone using date-fns-tz.
The architecture uses Next.js App Router with a public booking page (no auth required), authenticated dashboard pages, an API route for the Google Calendar OAuth callback, Server Actions for booking mutations, and an API route for slot computation that handles the timezone math server-side.
Final result
A scheduling app with a public booking page, weekly availability settings, configurable event types with buffer times, Google Calendar sync, and timezone-aware slot computation.
Tech stack
Prerequisites
- A V0 account (Premium recommended for the booking page and dashboard complexity)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Google Cloud project with Calendar API enabled and OAuth 2.0 credentials
Build steps
Set up the project and scheduling schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the schema for availability, event types, bookings, and calendar connections.
1// Paste this prompt into V0's AI chat:2// Build a scheduling app. Create a Supabase schema with:3// 1. availability: id (uuid PK), user_id (uuid FK), day_of_week (integer CHECK 0-6), start_time (time), end_time (time), timezone (text), created_at (timestamptz)4// 2. event_types: id (uuid PK), user_id (uuid FK), name (text), slug (text UNIQUE), duration_minutes (integer), buffer_minutes (integer DEFAULT 0), color (text), description (text), is_active (boolean DEFAULT true), created_at (timestamptz)5// 3. bookings: id (uuid PK), event_type_id (uuid FK), host_id (uuid FK), guest_name (text), guest_email (text), guest_phone (text), start_time (timestamptz), end_time (timestamptz), status (text CHECK confirmed/cancelled/completed/no_show), google_event_id (text), notes (text), created_at (timestamptz)6// 4. calendar_connections: id (uuid PK), user_id (uuid FK), provider (text CHECK google/outlook), access_token (text), refresh_token (text), token_expires_at (timestamptz), created_at (timestamptz)7// RLS: users manage own availability and event types. Bookings: host can read all, guests can insert (public booking page needs no auth for INSERT).8// Generate SQL migration and TypeScript types.Pro tip: The bookings table needs a special RLS policy: anyone can INSERT (for the public booking page) but only the host can SELECT and UPDATE. Use a policy like: CREATE POLICY "public_insert" ON bookings FOR INSERT WITH CHECK (true); CREATE POLICY "host_read" ON bookings FOR SELECT USING (host_id = auth.uid()).
Expected result: Supabase is connected with availability, event_types, bookings, and calendar_connections tables. RLS allows public booking inserts while restricting reads to the host.
Build the public booking page with slot computation
Create the public booking page where guests select a date, see available time slots, and book a meeting. The slot computation API subtracts existing bookings and buffer times from the host's availability.
1// Paste this prompt into V0's AI chat:2// Build a public booking page at app/[username]/[slug]/page.tsx.3// This page requires NO authentication — guests book directly.4// Requirements:5// - Left side: event type Card showing name, duration, description, host name6// - Right side: shadcn/ui Calendar for date selection7// - Below calendar: available time slots as a grid of Buttons8// - Slots come from GET /api/bookings/available-slots?event_type_id=X&date=YYYY-MM-DD&timezone=X9// - On slot click: slide to confirmation form with guest_name Input, guest_email Input, guest_phone Input, notes Textarea10// - "Confirm Booking" Button calls Server Action createBooking()11// - After booking: success Card with date, time, and "Add to Calendar" link12// - 'use client' for Calendar and slot selection13//14// Available slots API at app/api/bookings/available-slots/route.ts:15// - Fetches host's availability for the selected day_of_week16// - Generates all possible slots at duration_minutes intervals17// - Fetches existing bookings for the date (include buffer_minutes before/after)18// - Subtracts conflicts19// - Converts remaining slots to guest's timezone using date-fns-tz20// - Returns array of { start: string, end: string } in guest timezone21//22// Use shadcn/ui Calendar, Card, Button, Input, Textarea, BadgeExpected result: A public booking page with date selection, available time slot grid, guest information form, and confirmation. The API computes slots by subtracting existing bookings from availability.
Create the availability editor and event type manager
Build the dashboard pages for setting weekly availability (which days and times the host is available) and managing event types (meeting types with different durations).
1// Paste this prompt into V0's AI chat:2// Build availability and event type management pages.3//4// Availability page at app/dashboard/availability/page.tsx:5// - 7 rows for each day of week (Sunday-Saturday)6// - Each row: day name, Switch to enable/disable, start_time Select, end_time Select (15-min increments)7// - Timezone Select at top (with common timezones: US/Eastern, US/Pacific, Europe/London, etc.)8// - Server Action updateAvailability() saves all 7 days at once (delete + re-insert pattern)9// - Visual weekly calendar preview showing enabled hours10// - Use shadcn/ui Switch, Select, Card, Button11//12// Event types page at app/dashboard/event-types/page.tsx:13// - Grid of event type Cards showing name, duration Badge, buffer info, color dot14// - Switch on each Card to toggle is_active15// - "Create Event Type" Button opens Dialog:16// - name Input, slug Input (auto-generated from name), duration_minutes Select (15/30/45/60/90/120)17// - buffer_minutes Select (0/5/10/15/30), color picker, description Textarea18// - Each Card has booking link: copy to clipboard Button showing /[username]/[slug]19// - Server Actions: createEventType(), updateEventType(), deleteEventType()20// - Use shadcn/ui Card, Badge, Switch, Dialog, Input, Select, Textarea, Button, ToastExpected result: An availability editor with day-of-week toggles and time range selectors, plus an event type manager with create/edit/toggle Cards and copyable booking links.
Add Google Calendar sync and booking dashboard
Build the Google Calendar OAuth integration that syncs bookings as calendar events, plus the dashboard showing upcoming bookings in a calendar view.
1// Paste this prompt into V0's AI chat:2// Build Google Calendar integration and a booking dashboard.3//4// Google Calendar OAuth at app/api/calendar/google/callback/route.ts:5// - Handles the OAuth callback: exchanges code for tokens6// - Stores access_token, refresh_token, token_expires_at in calendar_connections7// - Redirects back to dashboard with success Toast8//9// Calendar sync at app/api/calendar/sync/route.ts:10// - Called after booking creation11// - Fetches host's Google Calendar tokens from calendar_connections12// - Refreshes token if expired13// - Creates a Google Calendar event with booking details (guest name, email, time)14// - Stores the google_event_id in the bookings row15//16// Dashboard at app/dashboard/page.tsx:17// - Weekly calendar view showing all confirmed bookings as colored blocks18// - Upcoming bookings list: Table with guest_name, event_type Badge, date, time, status Badge19// - Booking actions: Cancel (Dialog with confirmation), Mark Complete, Mark No-Show20// - "Connect Google Calendar" Button if no calendar_connections exist21// - Links to Google OAuth URL with scope calendar.events22// - Server Actions: cancelBooking(), updateBookingStatus()23// - Use shadcn/ui Card, Table, Badge, Button, Dialog, Calendar, Toast24//25// Env vars in V0's Vars tab: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (server-only, no NEXT_PUBLIC_)26// NEXT_PUBLIC_APP_URL (needed for OAuth redirect URI and booking page links)Pro tip: The Google Calendar OAuth redirect URI must point to your production URL (/api/calendar/google/callback). Set this in Google Cloud Console after your first Vercel deploy. During development, you can add http://localhost:3000 as an additional redirect URI.
Expected result: Google Calendar OAuth integration that syncs bookings as calendar events, plus a dashboard with weekly calendar view and booking management.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { addMinutes, format, parse, isWithinInterval, startOfDay, endOfDay } from 'date-fns'4import { toZonedTime, fromZonedTime } from 'date-fns-tz'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function GET(req: NextRequest) {12 const { searchParams } = new URL(req.url)13 const eventTypeId = searchParams.get('event_type_id')!14 const dateStr = searchParams.get('date')!15 const guestTz = searchParams.get('timezone') || 'America/New_York'1617 const { data: eventType } = await supabase18 .from('event_types')19 .select('*, availability:user_id(availability(*))')20 .eq('id', eventTypeId)21 .single()2223 if (!eventType) {24 return NextResponse.json({ error: 'Event type not found' }, { status: 404 })25 }2627 const hostTz = eventType.availability?.availability?.[0]?.timezone || 'UTC'28 const date = new Date(dateStr)29 const dayOfWeek = toZonedTime(date, hostTz).getDay()3031 const dayAvailability = (eventType.availability?.availability || []).find(32 (a: { day_of_week: number }) => a.day_of_week === dayOfWeek33 )3435 if (!dayAvailability) {36 return NextResponse.json({ slots: [] })37 }3839 const hostDate = format(toZonedTime(date, hostTz), 'yyyy-MM-dd')40 const startDt = fromZonedTime(41 parse(`${hostDate} ${dayAvailability.start_time}`, 'yyyy-MM-dd HH:mm:ss', new Date()),42 hostTz43 )44 const endDt = fromZonedTime(45 parse(`${hostDate} ${dayAvailability.end_time}`, 'yyyy-MM-dd HH:mm:ss', new Date()),46 hostTz47 )4849 const { data: existingBookings } = await supabase50 .from('bookings')51 .select('start_time, end_time')52 .eq('host_id', eventType.user_id)53 .eq('status', 'confirmed')54 .gte('start_time', startOfDay(date).toISOString())55 .lte('end_time', endOfDay(date).toISOString())5657 const buffer = eventType.buffer_minutes || 058 const duration = eventType.duration_minutes59 const slots: { start: string; end: string }[] = []60 let cursor = startDt6162 while (addMinutes(cursor, duration) <= endDt) {63 const slotStart = cursor64 const slotEnd = addMinutes(cursor, duration)65 const bufferedStart = addMinutes(slotStart, -buffer)66 const bufferedEnd = addMinutes(slotEnd, buffer)6768 const hasConflict = (existingBookings || []).some((booking) => {69 const bStart = new Date(booking.start_time)70 const bEnd = new Date(booking.end_time)71 return bufferedStart < bEnd && bufferedEnd > bStart72 })7374 if (!hasConflict && slotStart > new Date()) {75 const guestStart = toZonedTime(slotStart, guestTz)76 const guestEnd = toZonedTime(slotEnd, guestTz)77 slots.push({78 start: format(guestStart, "HH:mm"),79 end: format(guestEnd, "HH:mm"),80 })81 }8283 cursor = addMinutes(cursor, duration)84 }8586 return NextResponse.json({ slots })87}Customization ideas
Add email confirmations with Resend
Send booking confirmation emails to both the host and guest with meeting details, calendar invite (.ics attachment), and a cancellation link.
Build recurring availability exceptions
Let hosts block specific dates (vacations, holidays) that override the regular weekly availability, stored in a date_overrides table.
Add team scheduling
Extend to team use where a booking request is routed to the first available team member using round-robin or least-recently-booked logic.
Build payment collection
Integrate Stripe to charge for appointments at booking time, with automatic refunds on host-initiated cancellations.
Common pitfalls
Pitfall: Computing available slots on the client instead of the server
How to avoid: Use an API route (app/api/bookings/available-slots/route.ts) that computes slots server-side with the service role key, returning only the final available slots to the client.
Pitfall: Not accounting for buffer minutes when checking for conflicts
How to avoid: Expand each existing booking's time range by buffer_minutes on both sides when checking for conflicts. A 10-minute buffer means a meeting ending at 2:00 blocks slots until 2:10.
Pitfall: Storing times in the host's local timezone instead of UTC
How to avoid: Store all booking times as timestamptz (UTC). The availability table stores time-of-day in the host's timezone for weekly scheduling, but all bookings and comparisons use UTC. Convert to guest timezone only for display using date-fns-tz.
Best practices
- Compute available slots server-side in an API route — never expose the host's full calendar to the client
- Store booking times as timestamptz (UTC) and use date-fns-tz for timezone conversions on display only
- Account for buffer_minutes on both sides of existing bookings when computing available slots
- Store GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in V0's Vars tab without NEXT_PUBLIC_ prefix — they are server-only
- Use V0's Design Mode (Option+D) to adjust the booking page layout and Calendar styling without spending credits
- Allow public INSERT on the bookings table via RLS while restricting SELECT to the host for the public booking flow
AI prompts to try
Copy these prompts to build this project faster.
I'm building a scheduling app with Next.js and Supabase. Write a function that computes available time slots for a given date. It takes the host's weekly availability (day_of_week, start_time, end_time, timezone), event duration in minutes, buffer minutes, and an array of existing bookings (start_time, end_time as UTC timestamps). It should generate all possible slots, subtract conflicts including buffer time, filter out past slots, and return the remaining slots converted to a given guest timezone using date-fns-tz.
Create a time slot picker component. Accept an array of available slots (start: string, end: string) and a selected slot state. Render slots as a responsive grid of Buttons. The selected slot should have a ring and primary variant. Show a 'No slots available' message when the array is empty. Use Skeleton components while loading. Mark as 'use client' for the interactive selection state.
Frequently asked questions
Can I build this on V0's free tier?
V0 Free provides enough credits for the basic booking page and dashboard. Premium ($20/month) is recommended because this project has a public booking page, availability editor, event type manager, and calendar integration that benefit from prompt queuing.
How does the slot computation handle timezones?
The host sets availability in their local timezone. The API route generates slots in the host's timezone, checks conflicts in UTC, then converts the remaining available slots to the guest's timezone using date-fns-tz. All bookings are stored as UTC timestamptz.
Do guests need to create an account to book?
No. The public booking page at /[username]/[slug] requires no authentication. Guests enter their name, email, and optionally phone number. The bookings table has an RLS policy that allows public INSERT while restricting reads to the host.
How does Google Calendar sync work?
The host connects their Google account via OAuth. When a booking is confirmed, the system creates a Google Calendar event using the Calendar API with the booking details. The google_event_id is stored for future updates or cancellations.
How do I deploy the scheduling app?
Click Share then Publish to Production in V0. After deploying, update the Google Cloud Console OAuth redirect URI to https://yourdomain.vercel.app/api/calendar/google/callback. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (server-only), and NEXT_PUBLIC_APP_URL in V0's Vars tab.
Can RapidDev help build a custom scheduling platform?
Yes. RapidDev has built 600+ apps including scheduling platforms with team round-robin, payment collection, and multi-calendar sync. Book a free consultation to discuss your requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation