Skip to main content
RapidDev - Software Development Agency

How to Build Vehicle rentals backend with V0

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.

What you'll build

  • Vehicle fleet browsing page with shadcn/ui Card grid showing make, model, daily rate, and type Badge
  • Date range picker using shadcn DatePickerWithRange for selecting rental period with availability filtering
  • Booking creation flow with Stripe Payment Intent for deposit and Dialog confirmation
  • PostgreSQL EXCLUDE constraint using gist index to prevent double-bookings at the database level
  • Supabase RPC function available_vehicles(start_date, end_date) for real-time availability checking
  • Admin fleet management page with Table for vehicle status, mileage, and maintenance tracking
  • Customer booking history page with status Badges for pending, confirmed, active, and completed reservations
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium (Supabase + Stripe integrations)April 2026RapidDev Engineering Team
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.

What you're building

Car rental agencies and peer-to-peer vehicle sharing platforms need a system where customers browse available vehicles, select dates, and book without conflicts. The critical challenge is preventing double-bookings — two customers reserving the same car for overlapping dates.

V0 generates the fleet browsing UI, booking forms, and admin dashboard from prompts. Supabase stores vehicles, bookings, and reviews. PostgreSQL's EXCLUDE constraint with gist index guarantees no overlapping bookings at the database level. Stripe handles payment deposits.

The architecture uses Server Components for the fleet page, a Supabase RPC function for availability queries, Server Actions for booking management, and an API route for Stripe Payment Intent creation.

Final result

A vehicle rental platform with fleet browsing, date-range availability checking, double-booking prevention, Stripe payment deposits, and admin fleet management.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase & Auth
StripePayment Processing

Prerequisites

  • 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

Build steps

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.

supabase/migrations/001_schema.sql
1-- Enable btree_gist for EXCLUDE constraints
2CREATE EXTENSION IF NOT EXISTS btree_gist;
3
4CREATE TABLE vehicles (
5 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6 make text NOT NULL,
7 model text NOT NULL,
8 year int NOT NULL,
9 type text NOT NULL CHECK (type IN ('sedan','suv','truck','van','luxury')),
10 daily_rate_cents int NOT NULL,
11 image_url text,
12 plate_number text UNIQUE NOT NULL,
13 mileage int DEFAULT 0,
14 status text DEFAULT 'available' CHECK (status IN ('available','rented','maintenance')),
15 features jsonb DEFAULT '[]',
16 created_at timestamptz DEFAULT now()
17);
18
19CREATE TABLE bookings (
20 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
21 vehicle_id uuid REFERENCES vehicles(id) ON DELETE CASCADE,
22 customer_id uuid NOT NULL,
23 start_date date NOT NULL,
24 end_date date NOT NULL,
25 total_cents int NOT NULL,
26 status text DEFAULT 'pending' CHECK (status IN ('pending','confirmed','active','completed','cancelled')),
27 stripe_payment_intent_id text,
28 created_at timestamptz DEFAULT now(),
29 CONSTRAINT no_overlap EXCLUDE USING gist (
30 vehicle_id WITH =,
31 daterange(start_date, end_date) WITH &&
32 ) WHERE (status NOT IN ('cancelled'))
33);
34
35CREATE TABLE reviews (
36 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
37 booking_id uuid REFERENCES bookings(id) UNIQUE,
38 rating int CHECK (rating >= 1 AND rating <= 5),
39 comment text,
40 created_at timestamptz DEFAULT now()
41);

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/migrations/002_rpc.sql
1-- Supabase RPC function for availability checking
2CREATE OR REPLACE FUNCTION available_vehicles(p_start date, p_end date)
3RETURNS SETOF vehicles AS $$
4 SELECT v.*
5 FROM vehicles v
6 WHERE v.status = 'available'
7 AND NOT EXISTS (
8 SELECT 1 FROM bookings b
9 WHERE b.vehicle_id = v.id
10 AND b.status NOT IN ('cancelled')
11 AND daterange(b.start_date, b.end_date) && daterange(p_start, p_end)
12 )
13 ORDER BY v.daily_rate_cents ASC;
14$$ 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.

app/api/bookings/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@/lib/supabase/server'
3import Stripe from 'stripe'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6
7export async function POST(req: NextRequest) {
8 const supabase = await createClient()
9 const { vehicleId, startDate, endDate } = await req.json()
10
11 const { data: vehicle } = await supabase
12 .from('vehicles')
13 .select('daily_rate_cents, make, model')
14 .eq('id', vehicleId)
15 .single()
16
17 if (!vehicle) {
18 return NextResponse.json({ error: 'Vehicle not found' }, { status: 404 })
19 }
20
21 const days = Math.ceil(
22 (new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000
23 )
24 const totalCents = vehicle.daily_rate_cents * days
25
26 const paymentIntent = await stripe.paymentIntents.create({
27 amount: totalCents,
28 currency: 'usd',
29 metadata: { vehicleId, startDate, endDate },
30 })
31
32 const { data: booking, error } = await supabase
33 .from('bookings')
34 .insert({
35 vehicle_id: vehicleId,
36 customer_id: (await supabase.auth.getUser()).data.user?.id,
37 start_date: startDate,
38 end_date: endDate,
39 total_cents: totalCents,
40 stripe_payment_intent_id: paymentIntent.id,
41 })
42 .select()
43 .single()
44
45 if (error?.code === '23P01') {
46 return NextResponse.json(
47 { error: 'Vehicle is not available for these dates' },
48 { status: 409 }
49 )
50 }
51
52 return NextResponse.json({
53 bookingId: booking?.id,
54 clientSecret: paymentIntent.client_secret,
55 })
56}

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.

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

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

app/api/bookings/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@/lib/supabase/server'
3import Stripe from 'stripe'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6
7export async function POST(req: NextRequest) {
8 const supabase = await createClient()
9 const { vehicleId, startDate, endDate } = await req.json()
10
11 const { data: vehicle } = await supabase
12 .from('vehicles')
13 .select('daily_rate_cents, make, model')
14 .eq('id', vehicleId)
15 .single()
16
17 if (!vehicle) {
18 return NextResponse.json({ error: 'Vehicle not found' }, { status: 404 })
19 }
20
21 const days = Math.ceil(
22 (new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000
23 )
24 const totalCents = vehicle.daily_rate_cents * days
25
26 const paymentIntent = await stripe.paymentIntents.create({
27 amount: totalCents,
28 currency: 'usd',
29 metadata: { vehicleId, startDate, endDate },
30 })
31
32 const { data: booking, error } = await supabase
33 .from('bookings')
34 .insert({
35 vehicle_id: vehicleId,
36 customer_id: (await supabase.auth.getUser()).data.user?.id,
37 start_date: startDate,
38 end_date: endDate,
39 total_cents: totalCents,
40 stripe_payment_intent_id: paymentIntent.id,
41 })
42 .select()
43 .single()
44
45 if (error?.code === '23P01') {
46 return NextResponse.json(
47 { error: 'Vehicle is not available for these dates' },
48 { status: 409 }
49 )
50 }
51
52 return NextResponse.json({
53 bookingId: booking?.id,
54 clientSecret: paymentIntent.client_secret,
55 })
56}

Customization ideas

Add vehicle image gallery

Store multiple images per vehicle in Supabase Storage and display them in a carousel on the vehicle detail page using shadcn/ui's Carousel component.

Add GPS tracking integration

Integrate with a GPS tracking API to show real-time vehicle locations on a map for the admin dashboard, helping with fleet utilization monitoring.

Add dynamic pricing

Implement surge pricing based on demand. Calculate a multiplier from the ratio of booked-to-available vehicles for the selected dates and adjust the daily rate accordingly.

Add damage reporting

Create a check-in/check-out flow where customers photograph the vehicle before and after the rental. Store images in Supabase Storage with timestamps for dispute resolution.

Add loyalty program

Track rental days per customer and offer discount tiers (5% after 10 days, 10% after 30 days). Apply discounts automatically during booking.

Common pitfalls

Pitfall: Checking availability only in application code without database constraints

How to avoid: 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.

Pitfall: Not filtering cancelled bookings from the EXCLUDE constraint

How to avoid: Add WHERE (status NOT IN ('cancelled')) to the EXCLUDE constraint so cancelled bookings are ignored during overlap checks.

Pitfall: Calculating rental days with simple date subtraction

How to avoid: 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.

Pitfall: Exposing STRIPE_SECRET_KEY with NEXT_PUBLIC_ prefix

How to avoid: 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

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a vehicle rental platform with Next.js App Router and Supabase. I need: 1) A PostgreSQL EXCLUDE constraint using gist to prevent double-bookings on overlapping date ranges, 2) An RPC function to query available vehicles for a date range, 3) Stripe Payment Intent creation for booking deposits, 4) Handling the 23P01 exclusion_violation error gracefully. Help me design the schema and booking flow.

Build Prompt

Create a booking API route at app/api/bookings/route.ts that: 1) Receives vehicleId, startDate, endDate, 2) Fetches vehicle daily_rate_cents from Supabase, 3) Calculates total from date difference, 4) Creates a Stripe Payment Intent, 5) Inserts the booking with stripe_payment_intent_id, 6) Catches PostgreSQL error 23P01 (exclusion_violation) and returns 409 with 'dates unavailable', 7) Returns clientSecret for frontend payment confirmation.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.