# How to Build Vehicle rentals backend with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium (Supabase + Stripe integrations)
- Last updated: April 2026

## TL;DR

Build a vehicle rental platform with V0 featuring fleet browsing by date range, PostgreSQL EXCLUDE constraint for double-booking prevention, Stripe Payment Intent for deposits, and an admin fleet management dashboard. You'll create availability checking via Supabase RPC, date-range booking with Calendar, and reservation management — all in about 1-2 hours.

## Before you start

- A V0 account (Premium recommended for Supabase + Stripe integrations)
- A Supabase project with the btree_gist extension enabled (free tier works)
- A Stripe account with test mode API keys (publishable + secret)
- No additional API keys or accounts needed

## Step-by-step guide

### 1. Set up the vehicles, bookings, and reviews database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the vehicles, bookings, and reviews tables. The key is the EXCLUDE constraint on bookings that uses PostgreSQL's range exclusion to prevent overlapping date ranges for the same vehicle.

```
-- Enable btree_gist for EXCLUDE constraints
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE vehicles (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  make text NOT NULL,
  model text NOT NULL,
  year int NOT NULL,
  type text NOT NULL CHECK (type IN ('sedan','suv','truck','van','luxury')),
  daily_rate_cents int NOT NULL,
  image_url text,
  plate_number text UNIQUE NOT NULL,
  mileage int DEFAULT 0,
  status text DEFAULT 'available' CHECK (status IN ('available','rented','maintenance')),
  features jsonb DEFAULT '[]',
  created_at timestamptz DEFAULT now()
);

CREATE TABLE bookings (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  vehicle_id uuid REFERENCES vehicles(id) ON DELETE CASCADE,
  customer_id uuid NOT NULL,
  start_date date NOT NULL,
  end_date date NOT NULL,
  total_cents int NOT NULL,
  status text DEFAULT 'pending' CHECK (status IN ('pending','confirmed','active','completed','cancelled')),
  stripe_payment_intent_id text,
  created_at timestamptz DEFAULT now(),
  CONSTRAINT no_overlap EXCLUDE USING gist (
    vehicle_id WITH =,
    daterange(start_date, end_date) WITH &&
  ) WHERE (status NOT IN ('cancelled'))
);

CREATE TABLE reviews (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  booking_id uuid REFERENCES bookings(id) UNIQUE,
  rating int CHECK (rating >= 1 AND rating <= 5),
  comment text,
  created_at timestamptz DEFAULT now()
);
```

> Pro tip: The EXCLUDE constraint with WHERE (status NOT IN ('cancelled')) means cancelled bookings are ignored when checking for overlaps. This lets customers cancel and rebook the same dates without constraint violations.

**Expected result:** Three tables created with a gist EXCLUDE constraint that prevents any two non-cancelled bookings for the same vehicle from having overlapping date ranges.

### 2. Create the availability RPC function and fleet browsing page

Create a Supabase RPC function that returns vehicles with no overlapping confirmed bookings for a given date range. Then build the fleet browsing page with vehicle cards and date filtering.

```
-- Supabase RPC function for availability checking
CREATE OR REPLACE FUNCTION available_vehicles(p_start date, p_end date)
RETURNS SETOF vehicles AS $$
  SELECT v.*
  FROM vehicles v
  WHERE v.status = 'available'
    AND NOT EXISTS (
      SELECT 1 FROM bookings b
      WHERE b.vehicle_id = v.id
        AND b.status NOT IN ('cancelled')
        AND daterange(b.start_date, b.end_date) && daterange(p_start, p_end)
    )
  ORDER BY v.daily_rate_cents ASC;
$$ LANGUAGE sql STABLE;
```

> Pro tip: Use V0's prompt queuing to queue three prompts: the fleet page, the vehicle detail page, and the admin dashboard. They will generate in sequence while you review each one.

**Expected result:** The RPC function returns only vehicles with no booking conflicts for the selected date range. The fleet page shows vehicle Cards with type Badge, daily rate, and features.

### 3. Build the booking flow with Stripe Payment Intent

Create the vehicle detail page with a booking form that collects dates, calculates total cost, creates a Stripe Payment Intent for the deposit, and inserts the booking record.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: NextRequest) {
  const supabase = await createClient()
  const { vehicleId, startDate, endDate } = await req.json()

  const { data: vehicle } = await supabase
    .from('vehicles')
    .select('daily_rate_cents, make, model')
    .eq('id', vehicleId)
    .single()

  if (!vehicle) {
    return NextResponse.json({ error: 'Vehicle not found' }, { status: 404 })
  }

  const days = Math.ceil(
    (new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000
  )
  const totalCents = vehicle.daily_rate_cents * days

  const paymentIntent = await stripe.paymentIntents.create({
    amount: totalCents,
    currency: 'usd',
    metadata: { vehicleId, startDate, endDate },
  })

  const { data: booking, error } = await supabase
    .from('bookings')
    .insert({
      vehicle_id: vehicleId,
      customer_id: (await supabase.auth.getUser()).data.user?.id,
      start_date: startDate,
      end_date: endDate,
      total_cents: totalCents,
      stripe_payment_intent_id: paymentIntent.id,
    })
    .select()
    .single()

  if (error?.code === '23P01') {
    return NextResponse.json(
      { error: 'Vehicle is not available for these dates' },
      { status: 409 }
    )
  }

  return NextResponse.json({
    bookingId: booking?.id,
    clientSecret: paymentIntent.client_secret,
  })
}
```

> Pro tip: Error code 23P01 is PostgreSQL's exclusion_violation. Catching it lets you return a friendly 'dates unavailable' message when two users try to book the same vehicle simultaneously.

**Expected result:** Submitting a booking creates a Stripe Payment Intent and inserts the booking. If dates overlap with an existing booking, the EXCLUDE constraint rejects it with a 409 conflict response.

### 4. Build the admin fleet management dashboard

Create an admin page showing all vehicles in a Table with status management, booking overview, and the ability to mark vehicles for maintenance.

```
// Paste this prompt into V0's AI chat:
// Build an admin fleet management page at app/admin/fleet/page.tsx with:
// 1. Server Component fetching all vehicles with their active booking count
// 2. shadcn/ui Table with columns: Vehicle (make + model + year), Plate, Type Badge, Daily Rate, Mileage, Status Badge, Actions
// 3. Status Badge: 'available' = default, 'rented' = secondary, 'maintenance' = destructive
// 4. Action buttons: Edit (Dialog with Form), Toggle Maintenance, View Bookings
// 5. Summary Cards at top: total fleet size, currently rented, in maintenance, revenue this month
// 6. Server Actions for updateVehicleStatus and updateVehicleDetails
// 7. Filter by vehicle type using Select dropdown
// Use Supabase for all data fetching and mutations.
```

**Expected result:** An admin dashboard Table showing the entire fleet with status Badges, action buttons, and summary Cards for fleet metrics.

### 5. Build the customer bookings page with status tracking

Create a page where customers can view their booking history, see current reservations, and cancel pending bookings.

```
// Paste this prompt into V0's AI chat:
// Build a customer bookings page at app/bookings/page.tsx with:
// 1. Server Component fetching all bookings for the current user with vehicle details
// 2. shadcn/ui Card for each booking showing vehicle image, make/model, dates, total price
// 3. Badge for status: pending = outline, confirmed = default, active = secondary, completed = default, cancelled = destructive
// 4. Cancel button (AlertDialog confirmation) for pending bookings using Server Action
// 5. Review button (Dialog with star rating + textarea) for completed bookings
// 6. Tabs for Active, Upcoming, and Past bookings
// 7. Empty state with illustration when no bookings exist
// Cancel action should update booking status and the EXCLUDE constraint will free up those dates.
```

**Expected result:** A bookings page with tabbed views for active, upcoming, and past reservations. Customers can cancel pending bookings and leave reviews on completed ones.

## Complete code example

File: `app/api/bookings/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: NextRequest) {
  const supabase = await createClient()
  const { vehicleId, startDate, endDate } = await req.json()

  const { data: vehicle } = await supabase
    .from('vehicles')
    .select('daily_rate_cents, make, model')
    .eq('id', vehicleId)
    .single()

  if (!vehicle) {
    return NextResponse.json({ error: 'Vehicle not found' }, { status: 404 })
  }

  const days = Math.ceil(
    (new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000
  )
  const totalCents = vehicle.daily_rate_cents * days

  const paymentIntent = await stripe.paymentIntents.create({
    amount: totalCents,
    currency: 'usd',
    metadata: { vehicleId, startDate, endDate },
  })

  const { data: booking, error } = await supabase
    .from('bookings')
    .insert({
      vehicle_id: vehicleId,
      customer_id: (await supabase.auth.getUser()).data.user?.id,
      start_date: startDate,
      end_date: endDate,
      total_cents: totalCents,
      stripe_payment_intent_id: paymentIntent.id,
    })
    .select()
    .single()

  if (error?.code === '23P01') {
    return NextResponse.json(
      { error: 'Vehicle is not available for these dates' },
      { status: 409 }
    )
  }

  return NextResponse.json({
    bookingId: booking?.id,
    clientSecret: paymentIntent.client_secret,
  })
}
```

## Common mistakes

- **Checking availability only in application code without database constraints** — Two simultaneous booking requests can both pass the availability check before either inserts, creating a double-booking race condition. Fix: Use PostgreSQL's EXCLUDE constraint with gist index. It prevents overlapping date ranges at the database level, making double-bookings physically impossible regardless of concurrent requests.
- **Not filtering cancelled bookings from the EXCLUDE constraint** — Without the WHERE clause, cancelled bookings permanently block those dates. Customers can never rebook a cancelled period. Fix: Add WHERE (status NOT IN ('cancelled')) to the EXCLUDE constraint so cancelled bookings are ignored during overlap checks.
- **Calculating rental days with simple date subtraction** — Date subtraction without ceiling can return 0 for same-day rentals or fractional days, leading to incorrect pricing. Fix: Use Math.ceil() on the millisecond difference divided by 86400000, and enforce a minimum of 1 day in both the UI validation and the API.
- **Exposing STRIPE_SECRET_KEY with NEXT_PUBLIC_ prefix** — The secret key allows creating charges, refunds, and accessing customer data. Exposing it in the browser gives anyone full access to your Stripe account. Fix: Set STRIPE_SECRET_KEY in V0's Vars tab without any prefix. Only NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY should be exposed to the client.

## Best practices

- Enable the btree_gist extension before creating EXCLUDE constraints — it is required for combining equality and range operators in exclusion
- Use Supabase RPC functions for complex availability queries to keep logic in SQL where it performs best
- Create the Stripe Payment Intent server-side and only send the clientSecret to the frontend for confirmation
- Catch PostgreSQL error code 23P01 (exclusion_violation) specifically to return user-friendly availability messages
- Use V0's Connect panel to add both Supabase and Stripe via Vercel Marketplace for automatic env var configuration
- Use Design Mode (Option+D) to visually polish vehicle Cards and the booking form at zero credit cost
- Add RLS policies so customers can only see and cancel their own bookings while admins can manage all

## Frequently asked questions

### How does the EXCLUDE constraint prevent double-bookings?

The EXCLUDE constraint with gist index checks every INSERT against existing rows. If a new booking's vehicle_id matches an existing row AND the date ranges overlap (using the && operator), PostgreSQL rejects the insert with error 23P01. This happens at the database level, so even concurrent requests cannot create conflicts.

### What happens if two users try to book the same dates simultaneously?

PostgreSQL's EXCLUDE constraint handles this atomically. The first INSERT succeeds. The second triggers error 23P01 (exclusion_violation), which the API catches and returns a 409 'dates unavailable' response. No application-level locking is needed.

### Do I need the btree_gist extension?

Yes. The btree_gist extension is required for EXCLUDE constraints that combine equality operators (= for vehicle_id) with range operators (&& for daterange). Run CREATE EXTENSION IF NOT EXISTS btree_gist before creating the bookings table.

### What V0 plan do I need?

V0 Premium ($20/month) is recommended because it gives enough credits for building the fleet page, booking flow, and admin dashboard. The Supabase and Stripe integrations are configured through V0's Connect panel.

### How do I handle payment refunds for cancelled bookings?

When a customer cancels, use the stored stripe_payment_intent_id to create a refund via stripe.refunds.create({ payment_intent: booking.stripe_payment_intent_id }). The booking status changes to 'cancelled' and the EXCLUDE constraint immediately frees those dates for rebooking.

### How do I deploy this?

Click Share and then Publish in V0. Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in V0's Vars tab (no NEXT_PUBLIC_ prefix). The Supabase connection is auto-configured via the Connect panel. Register webhook endpoints in the Stripe dashboard using the production Vercel URL.

### Can RapidDev help build a custom vehicle rental platform?

Yes. RapidDev has built 600+ apps including fleet management platforms with real-time availability, multi-location support, and payment processing. Book a free consultation to discuss your rental platform requirements.

---

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