# How to Build a Vehicle Rentals Backend with Lovable

- Tool: How to Build with Lovable
- Difficulty: Intermediate
- Compatibility: Lovable Pro or higher
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

## Step-by-step guide

### 1. 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.

```
-- Run in Supabase SQL Editor
create table vehicles (
  id uuid primary key default gen_random_uuid(),
  make text not null,
  model text not null,
  year int,
  license_plate text unique,
  daily_rate numeric(10,2) not null,
  category text default 'sedan' check (category in ('sedan','suv','truck','van','luxury','electric')),
  status text default 'available' check (status in ('available','maintenance','retired')),
  photo_path text,
  created_at timestamptz default now()
);

create table renters (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id),
  full_name text not null,
  email text not null,
  phone text,
  license_number text,
  created_at timestamptz default now()
);

create table bookings (
  id uuid primary key default gen_random_uuid(),
  vehicle_id uuid references vehicles(id) on delete restrict,
  renter_id uuid references renters(id) on delete restrict,
  start_date date not null,
  end_date date not null,
  total_amount numeric(10,2),
  status text default 'pending' check (status in ('pending','confirmed','active','completed','cancelled')),
  notes text,
  created_at timestamptz default now(),
  constraint valid_dates check (end_date > start_date)
);

create index idx_bookings_vehicle_dates on bookings(vehicle_id, start_date, end_date);

-- Availability check function
create or replace function check_vehicle_availability(
  p_vehicle_id uuid,
  p_start date,
  p_end date,
  p_exclude_booking_id uuid default null
) returns boolean language sql as $$
  select not exists (
    select 1 from bookings
    where vehicle_id = p_vehicle_id
      and status not in ('cancelled')
      and (p_exclude_booking_id is null or id != p_exclude_booking_id)
      and start_date < p_end
      and end_date > p_start
  );
$$;

-- RLS
alter table vehicles enable row level security;
alter table renters enable row level security;
alter table bookings enable row level security;

create policy "Public read vehicles" on vehicles for select using (true);
create policy "Auth read renters own" on renters for select using (auth.uid() = user_id);
create policy "Auth manage own bookings" on bookings for all using (renter_id in (select id from renters where user_id = auth.uid()));
create 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.

### 2. 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.

```
Build a /fleet page showing all vehicles as a responsive grid (3 columns on desktop, 1 on mobile).

For each vehicle, render a shadcn Card with:
- Top: vehicle photo from Supabase Storage (photo_path field), aspect-video, object-cover, rounded top
- If no photo, show a gray placeholder with a car icon
- Body: '{year} {make} {model}' as the title
- Category Badge (sedan=blue, suv=green, truck=orange, luxury=purple, electric=teal)
- Daily rate in large text: '$X.XX / day'
- Status: green dot 'Available' or red dot 'In Maintenance'
- 'Book Now' Button (disabled if status != 'available')

Fetch vehicles from Supabase ordered by daily_rate ascending.
Add a filter bar above the grid with category Select and max price Slider.
Clicking 'Book Now' opens the BookingDialog component.
All 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.

### 3. 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.

```
import { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import { Alert } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { DateRange } from 'react-day-picker'
import { format, differenceInDays } from 'date-fns'
import { supabase } from '@/lib/supabase'
import { toast } from 'sonner'

interface Vehicle {
  id: string
  make: string
  model: string
  year: number
  daily_rate: number
}

interface BookingDialogProps {
  vehicle: Vehicle | null
  onClose: () => void
  renterId: string
}

export function BookingDialog({ vehicle, onClose, renterId }: BookingDialogProps) {
  const [dateRange, setDateRange] = useState<DateRange | undefined>()
  const [available, setAvailable] = useState<boolean | null>(null)
  const [loading, setLoading] = useState(false)

  const nights = dateRange?.from && dateRange?.to
    ? differenceInDays(dateRange.to, dateRange.from)
    : 0
  const total = nights * (vehicle?.daily_rate ?? 0)

  const checkAvailability = async (range: DateRange) => {
    if (!range.from || !range.to || !vehicle) return
    const { data } = await supabase.rpc('check_vehicle_availability', {
      p_vehicle_id: vehicle.id,
      p_start: format(range.from, 'yyyy-MM-dd'),
      p_end: format(range.to, 'yyyy-MM-dd')
    })
    setAvailable(data as boolean)
  }

  const handleDateChange = (range: DateRange | undefined) => {
    setDateRange(range)
    setAvailable(null)
    if (range?.from && range?.to) checkAvailability(range)
  }

  const confirmBooking = async () => {
    if (!vehicle || !dateRange?.from || !dateRange?.to) return
    setLoading(true)
    const { error } = await supabase.from('bookings').insert({
      vehicle_id: vehicle.id,
      renter_id: renterId,
      start_date: format(dateRange.from, 'yyyy-MM-dd'),
      end_date: format(dateRange.to, 'yyyy-MM-dd'),
      total_amount: total,
      status: 'pending'
    })
    setLoading(false)
    if (error) { toast.error('Booking failed: ' + error.message); return }
    toast.success('Booking confirmed!')
    onClose()
  }

  return (
    <Dialog open={!!vehicle} onOpenChange={onClose}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>{vehicle?.year} {vehicle?.make} {vehicle?.model}</DialogTitle>
        </DialogHeader>
        <div className="space-y-4">
          <Popover>
            <PopoverTrigger asChild>
              <Button variant="outline" className="w-full justify-start">
                {dateRange?.from && dateRange?.to
                  ? `${format(dateRange.from, 'MMM d')} - ${format(dateRange.to, 'MMM d, yyyy')}`
                  : 'Select pickup and return dates'}
              </Button>
            </PopoverTrigger>
            <PopoverContent className="w-auto p-0" align="start">
              <Calendar mode="range" selected={dateRange} onSelect={handleDateChange}
                disabled={{ before: new Date() }} numberOfMonths={2} />
            </PopoverContent>
          </Popover>
          {available === false && (
            <Alert variant="destructive">These dates are not available. Please choose different dates.</Alert>
          )}
          {available === true && nights > 0 && (
            <div className="bg-muted rounded-lg p-4 space-y-1">
              <div className="flex justify-between text-sm"><span>{nights} nights</span><span>${vehicle?.daily_rate}/night</span></div>
              <div className="flex justify-between font-semibold"><span>Total</span><span>${total.toFixed(2)}</span></div>
            </div>
          )}
        </div>
        <DialogFooter>
          <Button variant="outline" onClick={onClose}>Cancel</Button>
          <Button onClick={confirmBooking} disabled={!available || loading}>Confirm Booking</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

> 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.

### 4. 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.

```
Build an /admin/bookings page (protected route, require admin role).

Fetch bookings with a join to vehicles (make, model) and renters (full_name, email).

Render a shadcn DataTable with columns:
- renter_name (text)
- vehicle ('{year} {make} {model}')
- start_date (formatted)
- end_date (formatted)
- nights (calculated: end_date - start_date)
- total_amount ('$X.XX')
- status Badge: pending=yellow, confirmed=blue, active=green, completed=gray, cancelled=red
- Actions: DropdownMenu with 'Confirm', 'Mark Active', 'Mark Completed', 'Cancel'

Above the table add:
- Status Select filter (all/pending/confirmed/active/completed/cancelled)
- Date range Popover filter for start_date
- Search Input filtering by renter name or vehicle

Updating status via the DropdownMenu does an immediate Supabase update and refreshes the row.
Show 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.

### 5. 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.

```
Add two features to the fleet:

1. Vehicle photo upload (admin only):
   - In the vehicle edit form, add a file Input (accept image/*)
   - On upload, send to Supabase Storage bucket 'vehicle-photos' (public bucket)
   - Use path: `${vehicleId}/cover.${extension}`
   - Store the public URL in vehicles.photo_path
   - Show a Progress bar during upload

2. Vehicle detail page at /fleet/:id:
   - Show the vehicle photo, full specs, and daily rate
   - Fetch all active bookings for this vehicle (status not in 'cancelled')
   - Pass booked date ranges to a Calendar component's disabled prop
   - Booked dates appear grayed out
   - Show a legend: gray = unavailable, white = available
   - 'Book This Vehicle' Button below the calendar opens BookingDialog

For 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 example

File: `src/lib/availability.ts`

```typescript
import { supabase } from '@/lib/supabase'
import { format, eachDayOfInterval, parseISO } from 'date-fns'

export interface BookedRange {
  start_date: string
  end_date: string
}

/** Fetch all booked date ranges for a vehicle (excluding cancelled bookings) */
export async function getBookedRanges(vehicleId: string): Promise<BookedRange[]> {
  const { data, error } = await supabase
    .from('bookings')
    .select('start_date, end_date')
    .eq('vehicle_id', vehicleId)
    .not('status', 'eq', 'cancelled')
    .gte('end_date', format(new Date(), 'yyyy-MM-dd'))
  if (error || !data) return []
  return data
}

/** Convert booked ranges to an array of disabled Date objects for shadcn Calendar */
export function bookedRangesToDisabledDates(ranges: BookedRange[]): Date[] {
  const dates: Date[] = []
  for (const range of ranges) {
    const days = eachDayOfInterval({
      start: parseISO(range.start_date),
      end: parseISO(range.end_date)
    })
    dates.push(...days)
  }
  return dates
}

/** Check availability by calling the Supabase RPC function */
export async function checkAvailability(
  vehicleId: string,
  startDate: Date,
  endDate: Date,
  excludeBookingId?: string
): Promise<boolean> {
  const { data, error } = await supabase.rpc('check_vehicle_availability', {
    p_vehicle_id: vehicleId,
    p_start: format(startDate, 'yyyy-MM-dd'),
    p_end: format(endDate, 'yyyy-MM-dd'),
    p_exclude_booking_id: excludeBookingId ?? null
  })
  if (error) {
    console.error('Availability check error:', error.message)
    return false
  }
  return data as boolean
}
```

## Common mistakes

- **Checking availability only in the frontend** — Two users could simultaneously select the same dates and both pass the frontend check before either insert reaches the database, causing a double-booking. Fix: 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'.
- **Using text fields instead of date type for start_date and end_date** — Text fields break date comparison queries and the overlap detection function. ISO string sorting is not the same as date comparison. Fix: Always use the date column type in Postgres for booking dates. The check_vehicle_availability function relies on standard date comparison operators.
- **Making the vehicle-photos bucket private** — Vehicle photos are public marketing content — making them private means generating signed URLs for every page load, which is slow and uses up Supabase function invocations. Fix: Use a public bucket for vehicle photos and a private bucket only for sensitive documents like renter license scans.
- **Not adding the valid_dates constraint** — Without the check constraint, end_date could be set before start_date, producing negative night counts and negative total amounts in the booking records. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/vehicle-rentals-backend
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/vehicle-rentals-backend
