Build a vehicle rentals backend in Lovable with a fleet management grid, date-range booking calendar, and admin DataTable. Uses a Supabase database function to detect availability conflicts and prevent double-bookings. Covers photo uploads to Storage, renter profiles, and a booking Dialog with price calculation.
What you're building
A vehicle rental management system with two distinct views: a customer-facing fleet browser and an admin booking dashboard.
Customers browse a Card grid of available vehicles with photos, daily rates, and availability badges. Selecting a vehicle opens a Dialog with a Calendar date-range picker. When dates are chosen, a Supabase function checks for overlapping bookings and returns either a confirmed price or a conflict error — preventing double-bookings at the database level rather than relying on frontend logic alone.
Admins get a DataTable listing every booking with status (Pending, Confirmed, Active, Completed, Cancelled), renter name, vehicle, dates, and total amount. Filtering by status, date range, or vehicle makes fleet management fast.
Final result
A production-ready vehicle rental backend where customers can browse vehicles, pick dates from an availability-aware calendar, and complete a booking — with double-booking prevention enforced by a Postgres function and full admin oversight via a filterable DataTable.
Tech stack
Prerequisites
- Supabase project created with Auth enabled
- Lovable Pro account for Storage bucket access
- SUPABASE_URL and SUPABASE_ANON_KEY ready for Lovable Secrets
- Basic understanding of date ranges — the booking logic relies on overlapping interval detection
Build steps
Create the Supabase schema with availability function
Set up the three core tables and the availability check function. The function uses a half-open interval overlap check — the standard SQL pattern for date conflicts — and is called before every booking insert.
1-- Run in Supabase SQL Editor2create table vehicles (3 id uuid primary key default gen_random_uuid(),4 make text not null,5 model text not null,6 year int,7 license_plate text unique,8 daily_rate numeric(10,2) not null,9 category text default 'sedan' check (category in ('sedan','suv','truck','van','luxury','electric')),10 status text default 'available' check (status in ('available','maintenance','retired')),11 photo_path text,12 created_at timestamptz default now()13);1415create table renters (16 id uuid primary key default gen_random_uuid(),17 user_id uuid references auth.users(id),18 full_name text not null,19 email text not null,20 phone text,21 license_number text,22 created_at timestamptz default now()23);2425create table bookings (26 id uuid primary key default gen_random_uuid(),27 vehicle_id uuid references vehicles(id) on delete restrict,28 renter_id uuid references renters(id) on delete restrict,29 start_date date not null,30 end_date date not null,31 total_amount numeric(10,2),32 status text default 'pending' check (status in ('pending','confirmed','active','completed','cancelled')),33 notes text,34 created_at timestamptz default now(),35 constraint valid_dates check (end_date > start_date)36);3738create index idx_bookings_vehicle_dates on bookings(vehicle_id, start_date, end_date);3940-- Availability check function41create or replace function check_vehicle_availability(42 p_vehicle_id uuid,43 p_start date,44 p_end date,45 p_exclude_booking_id uuid default null46) returns boolean language sql as $$47 select not exists (48 select 1 from bookings49 where vehicle_id = p_vehicle_id50 and status not in ('cancelled')51 and (p_exclude_booking_id is null or id != p_exclude_booking_id)52 and start_date < p_end53 and end_date > p_start54 );55$$;5657-- RLS58alter table vehicles enable row level security;59alter table renters enable row level security;60alter table bookings enable row level security;6162create policy "Public read vehicles" on vehicles for select using (true);63create policy "Auth read renters own" on renters for select using (auth.uid() = user_id);64create policy "Auth manage own bookings" on bookings for all using (renter_id in (select id from renters where user_id = auth.uid()));65create policy "Service role full access" on bookings for all using (auth.role() = 'service_role');Pro tip: The index on bookings(vehicle_id, start_date, end_date) is critical for performance. Without it, the availability function does a full table scan on every date picker interaction.
Expected result: Three tables created in Supabase. The check_vehicle_availability function appears under Database → Functions. RLS is active on all tables.
Build the vehicle fleet grid
Prompt Lovable to create the main fleet page with a responsive Card grid. Each card shows a vehicle photo from Storage, the make/model/year, category Badge, daily rate, and an availability status indicator.
1Build a /fleet page showing all vehicles as a responsive grid (3 columns on desktop, 1 on mobile).23For each vehicle, render a shadcn Card with:4- Top: vehicle photo from Supabase Storage (photo_path field), aspect-video, object-cover, rounded top5- If no photo, show a gray placeholder with a car icon6- Body: '{year} {make} {model}' as the title7- Category Badge (sedan=blue, suv=green, truck=orange, luxury=purple, electric=teal)8- Daily rate in large text: '$X.XX / day'9- Status: green dot 'Available' or red dot 'In Maintenance'10- 'Book Now' Button (disabled if status != 'available')1112Fetch vehicles from Supabase ordered by daily_rate ascending.13Add a filter bar above the grid with category Select and max price Slider.14Clicking 'Book Now' opens the BookingDialog component.15All TypeScript, use a Vehicle interface.Expected result: A grid of vehicle cards renders from Supabase data. Category filters narrow the results. The Book Now button is active only for available vehicles.
Build the booking Dialog with Calendar date picker
The booking Dialog uses a Popover Calendar for date selection. When dates are chosen, call the Supabase availability function and show either a price summary or a conflict warning before the user confirms.
1import { useState } from 'react'2import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'3import { Calendar } from '@/components/ui/calendar'4import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'5import { Button } from '@/components/ui/button'6import { Alert } from '@/components/ui/alert'7import { Badge } from '@/components/ui/badge'8import { DateRange } from 'react-day-picker'9import { format, differenceInDays } from 'date-fns'10import { supabase } from '@/lib/supabase'11import { toast } from 'sonner'1213interface Vehicle {14 id: string15 make: string16 model: string17 year: number18 daily_rate: number19}2021interface BookingDialogProps {22 vehicle: Vehicle | null23 onClose: () => void24 renterId: string25}2627export function BookingDialog({ vehicle, onClose, renterId }: BookingDialogProps) {28 const [dateRange, setDateRange] = useState<DateRange | undefined>()29 const [available, setAvailable] = useState<boolean | null>(null)30 const [loading, setLoading] = useState(false)3132 const nights = dateRange?.from && dateRange?.to33 ? differenceInDays(dateRange.to, dateRange.from)34 : 035 const total = nights * (vehicle?.daily_rate ?? 0)3637 const checkAvailability = async (range: DateRange) => {38 if (!range.from || !range.to || !vehicle) return39 const { data } = await supabase.rpc('check_vehicle_availability', {40 p_vehicle_id: vehicle.id,41 p_start: format(range.from, 'yyyy-MM-dd'),42 p_end: format(range.to, 'yyyy-MM-dd')43 })44 setAvailable(data as boolean)45 }4647 const handleDateChange = (range: DateRange | undefined) => {48 setDateRange(range)49 setAvailable(null)50 if (range?.from && range?.to) checkAvailability(range)51 }5253 const confirmBooking = async () => {54 if (!vehicle || !dateRange?.from || !dateRange?.to) return55 setLoading(true)56 const { error } = await supabase.from('bookings').insert({57 vehicle_id: vehicle.id,58 renter_id: renterId,59 start_date: format(dateRange.from, 'yyyy-MM-dd'),60 end_date: format(dateRange.to, 'yyyy-MM-dd'),61 total_amount: total,62 status: 'pending'63 })64 setLoading(false)65 if (error) { toast.error('Booking failed: ' + error.message); return }66 toast.success('Booking confirmed!')67 onClose()68 }6970 return (71 <Dialog open={!!vehicle} onOpenChange={onClose}>72 <DialogContent className="max-w-md">73 <DialogHeader>74 <DialogTitle>{vehicle?.year} {vehicle?.make} {vehicle?.model}</DialogTitle>75 </DialogHeader>76 <div className="space-y-4">77 <Popover>78 <PopoverTrigger asChild>79 <Button variant="outline" className="w-full justify-start">80 {dateRange?.from && dateRange?.to81 ? `${format(dateRange.from, 'MMM d')} - ${format(dateRange.to, 'MMM d, yyyy')}`82 : 'Select pickup and return dates'}83 </Button>84 </PopoverTrigger>85 <PopoverContent className="w-auto p-0" align="start">86 <Calendar mode="range" selected={dateRange} onSelect={handleDateChange}87 disabled={{ before: new Date() }} numberOfMonths={2} />88 </PopoverContent>89 </Popover>90 {available === false && (91 <Alert variant="destructive">These dates are not available. Please choose different dates.</Alert>92 )}93 {available === true && nights > 0 && (94 <div className="bg-muted rounded-lg p-4 space-y-1">95 <div className="flex justify-between text-sm"><span>{nights} nights</span><span>${vehicle?.daily_rate}/night</span></div>96 <div className="flex justify-between font-semibold"><span>Total</span><span>${total.toFixed(2)}</span></div>97 </div>98 )}99 </div>100 <DialogFooter>101 <Button variant="outline" onClick={onClose}>Cancel</Button>102 <Button onClick={confirmBooking} disabled={!available || loading}>Confirm Booking</Button>103 </DialogFooter>104 </DialogContent>105 </Dialog>106 )107}Pro tip: The Calendar's disabled prop can also accept an array of specific dates. Fetch all booked date ranges for the vehicle and pass them as disabled dates so the calendar visually grays out unavailable periods.
Expected result: The Calendar opens in a Popover. Selecting a conflicting date range shows a red Alert. A valid range shows the price summary and enables the Confirm button.
Build the admin bookings DataTable
The admin view shows all bookings in a filterable DataTable. Filters by status, vehicle, and date range let fleet managers track active rentals and pending approvals quickly.
1Build an /admin/bookings page (protected route, require admin role).23Fetch bookings with a join to vehicles (make, model) and renters (full_name, email).45Render a shadcn DataTable with columns:6- renter_name (text)7- vehicle ('{year} {make} {model}')8- start_date (formatted)9- end_date (formatted)10- nights (calculated: end_date - start_date)11- total_amount ('$X.XX')12- status Badge: pending=yellow, confirmed=blue, active=green, completed=gray, cancelled=red13- Actions: DropdownMenu with 'Confirm', 'Mark Active', 'Mark Completed', 'Cancel'1415Above the table add:16- Status Select filter (all/pending/confirmed/active/completed/cancelled)17- Date range Popover filter for start_date18- Search Input filtering by renter name or vehicle1920Updating status via the DropdownMenu does an immediate Supabase update and refreshes the row.21Show total revenue and active booking count in two Card components above the table.Expected result: The admin page shows all bookings with status filters. Changing a booking status from the dropdown updates Supabase and the Badge color changes immediately.
Add vehicle photo upload and blocked dates calendar
Complete the fleet management with photo uploads to Supabase Storage and a vehicle detail page showing a calendar with blocked dates visually indicated.
1Add two features to the fleet:231. Vehicle photo upload (admin only):4 - In the vehicle edit form, add a file Input (accept image/*)5 - On upload, send to Supabase Storage bucket 'vehicle-photos' (public bucket)6 - Use path: `${vehicleId}/cover.${extension}`7 - Store the public URL in vehicles.photo_path8 - Show a Progress bar during upload9102. Vehicle detail page at /fleet/:id:11 - Show the vehicle photo, full specs, and daily rate12 - Fetch all active bookings for this vehicle (status not in 'cancelled')13 - Pass booked date ranges to a Calendar component's disabled prop14 - Booked dates appear grayed out15 - Show a legend: gray = unavailable, white = available16 - 'Book This Vehicle' Button below the calendar opens BookingDialog1718For the public photo URL, use Supabase's getPublicUrl() method, not signed URLs.Expected result: Vehicle cards show uploaded photos. The detail page calendar visually grays out all booked date ranges. Customers can see availability before picking dates.
Complete code
1import { supabase } from '@/lib/supabase'2import { format, eachDayOfInterval, parseISO } from 'date-fns'34export interface BookedRange {5 start_date: string6 end_date: string7}89/** Fetch all booked date ranges for a vehicle (excluding cancelled bookings) */10export async function getBookedRanges(vehicleId: string): Promise<BookedRange[]> {11 const { data, error } = await supabase12 .from('bookings')13 .select('start_date, end_date')14 .eq('vehicle_id', vehicleId)15 .not('status', 'eq', 'cancelled')16 .gte('end_date', format(new Date(), 'yyyy-MM-dd'))17 if (error || !data) return []18 return data19}2021/** Convert booked ranges to an array of disabled Date objects for shadcn Calendar */22export function bookedRangesToDisabledDates(ranges: BookedRange[]): Date[] {23 const dates: Date[] = []24 for (const range of ranges) {25 const days = eachDayOfInterval({26 start: parseISO(range.start_date),27 end: parseISO(range.end_date)28 })29 dates.push(...days)30 }31 return dates32}3334/** Check availability by calling the Supabase RPC function */35export async function checkAvailability(36 vehicleId: string,37 startDate: Date,38 endDate: Date,39 excludeBookingId?: string40): Promise<boolean> {41 const { data, error } = await supabase.rpc('check_vehicle_availability', {42 p_vehicle_id: vehicleId,43 p_start: format(startDate, 'yyyy-MM-dd'),44 p_end: format(endDate, 'yyyy-MM-dd'),45 p_exclude_booking_id: excludeBookingId ?? null46 })47 if (error) {48 console.error('Availability check error:', error.message)49 return false50 }51 return data as boolean52}Customization ideas
Dynamic pricing by season
Add a pricing_rules table with date ranges and multipliers. Modify the BookingDialog to apply the multiplier when the selected dates fall within a peak season range.
Insurance and extras upsell
Add an extras table (insurance, GPS, child seat) with per-day rates. Show Checkbox items in the booking form and add selected extras to the total calculation.
SMS booking confirmation
Create a Supabase Edge Function triggered by a database webhook on bookings INSERT. Call Twilio to send an SMS to the renter's phone number with booking details and a cancellation link.
Vehicle maintenance scheduling
Add a maintenance_windows table. Block those dates in the availability calendar automatically and show an orange badge on the vehicle card during maintenance periods.
Damage report on return
When an admin marks a booking Completed, show a damage report Dialog with a photo upload and description field. Save reports to a vehicle_incidents table linked to the booking.
Common pitfalls
Pitfall: Checking availability only in the frontend
How to avoid: Always use the database function (check_vehicle_availability) called via Supabase RPC. Add a unique partial index as a second safety net: CREATE UNIQUE INDEX one_active_booking on bookings(vehicle_id, start_date) where status != 'cancelled'.
Pitfall: Using text fields instead of date type for start_date and end_date
How to avoid: Always use the date column type in Postgres for booking dates. The check_vehicle_availability function relies on standard date comparison operators.
Pitfall: Making the vehicle-photos bucket private
How to avoid: Use a public bucket for vehicle photos and a private bucket only for sensitive documents like renter license scans.
Pitfall: Not adding the valid_dates constraint
How to avoid: The schema in Step 1 includes CONSTRAINT valid_dates CHECK (end_date > start_date). Make sure to include this in your SQL setup.
Best practices
- Always validate date availability at the database level — never rely solely on frontend checks for booking conflicts
- Add a database-level unique constraint as a fallback in addition to the availability function for race condition protection
- Use the date type (not timestamptz) for booking dates to avoid timezone-related off-by-one errors in availability calculations
- Enable Supabase Realtime on the bookings table so the admin DataTable reflects new bookings without manual refresh
- Store vehicle photos with a predictable path pattern (vehicleId/cover.jpg) to make photo management and deletion straightforward
- Use shadcn Calendar's disabled prop with an array of booked dates so customers see visual feedback before selecting dates
- Index the bookings table on (vehicle_id, start_date, end_date) for fast availability queries on fleets with hundreds of vehicles
- Scope admin routes with a Supabase role check — verify the user has an admin record in a user_roles table before rendering the /admin pages
AI prompts to try
Copy these prompts to build this project faster.
Write a PostgreSQL function that takes a vehicle_id, start_date, and end_date and returns true if the vehicle has no overlapping bookings (excluding cancelled ones). Include the half-open interval overlap pattern and an optional exclude_booking_id parameter for editing existing bookings.
Build a vehicle rental platform with a /fleet Card grid showing vehicle photos, rates, and availability. Clicking a vehicle opens a BookingDialog with a date-range Calendar that calls a Supabase RPC function to check for conflicts before showing the total price and confirming.
Add an /admin/bookings DataTable that shows all bookings with vehicle name, renter name, dates, total amount, and a status Badge. Include a DropdownMenu on each row to update the booking status to confirmed, active, completed, or cancelled. The status change should update Supabase immediately and refresh the row.
Frequently asked questions
How does the availability check prevent double-bookings?
The check_vehicle_availability Postgres function uses a half-open interval overlap check: it looks for any existing booking where start_date < requested_end AND end_date > requested_start. This catches all overlap scenarios including partial overlaps. The check runs at the database level, so even if two users submit at the exact same millisecond, only one booking will succeed.
Can I build this on the Lovable free plan?
The core booking logic, DataTable, and Calendar all work on the free plan. Vehicle photo uploads require Supabase Storage access, which works on the free Supabase tier but needs a Lovable Pro account for full Storage integration in the UI. The free plan's 5 daily credits will be tight for a project this size — Pro is recommended.
How do I show booked dates as grayed out in the Calendar?
Fetch all non-cancelled bookings for the vehicle from Supabase, convert each date range into an array of individual dates using date-fns eachDayOfInterval, then pass the combined array to the Calendar component's disabled prop. The complete utility function is provided in the complete_code section of this guide.
How do I handle same-day returns and pickups?
The half-open interval (start <= date < end) means end_date is treated as the return day, not an additional night. If Vehicle A is returned on March 10 and Vehicle B picks up on March 10, there is no overlap because the check is strictly end_date > new_start_date. Make this clear to renters in the UI by labeling dates as 'Pickup Date' and 'Return Date'.
How do I deploy this with a custom domain?
Click the Publish icon in Lovable (top-right), then navigate to Settings → Custom Domain. Add your domain and follow the DNS CNAME instructions. Update your Supabase Auth settings to add the new domain to the allowed redirect URLs list so authentication continues to work after the domain change.
What if I need to edit a confirmed booking's dates?
Pass the existing booking's ID to the check_vehicle_availability function as p_exclude_booking_id. This tells the function to ignore the current booking when checking for conflicts, so editing dates works correctly without falsely detecting the booking as conflicting with itself.
How do I add Stripe payments at checkout?
See the related payment-gateway-integration guide. The pattern for vehicle rentals is: create a Stripe Checkout session in a Supabase Edge Function with the total_amount, redirect the user to Stripe's hosted page, and on checkout.session.completed webhook, update the booking status from pending to confirmed.
My Lovable build is producing booking logic errors — what should I do?
Use Lovable's Plan Mode to describe the exact conflict detection logic step by step before asking it to generate code. Plan Mode never modifies your existing code, so it is safe to use for complex logic design. If the issue persists, RapidDev can review your Supabase function and booking flow to identify where the conflict detection is breaking down.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation