Skip to main content
RapidDev - Software Development Agency

How to Build Ride hailing platform with V0

Build an Uber-style ride-hailing platform with V0 using Next.js and Supabase that matches riders with nearby drivers using PostGIS spatial queries, tracks rides in real time via Supabase Realtime, calculates fares, and processes payments with Stripe — all in about 2-4 hours.

What you'll build

  • Rider interface with pickup/dropoff selection and live driver tracking on an interactive map
  • Driver interface with ride accept/reject controls and navigation to pickup
  • Nearest-driver matching using PostGIS ST_DWithin with atomic status locking to prevent race conditions
  • Real-time ride status updates via Supabase Realtime subscriptions on rides and drivers tables
  • Fare calculation based on distance and duration with Stripe payment processing on ride completion
  • Post-ride rating system with mutual ratings and running averages for riders and drivers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced10 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build an Uber-style ride-hailing platform with V0 using Next.js and Supabase that matches riders with nearby drivers using PostGIS spatial queries, tracks rides in real time via Supabase Realtime, calculates fares, and processes payments with Stripe — all in about 2-4 hours.

What you're building

Ride-hailing is one of the most technically demanding real-time applications you can build. It combines geospatial queries, live location tracking, real-time state synchronization, and payment processing — all under strict timing constraints where a few seconds of delay ruin the user experience.

V0 generates the rider and driver interfaces, ride request flow, and map components from prompts. The key architectural challenge is nearest-driver matching under concurrent load. A Supabase RPC function uses PostGIS ST_DWithin to find drivers within 5km, ordered by distance, and atomically sets the matched driver's status to 'busy' inside a transaction — preventing two riders from being matched to the same driver.

The architecture uses Next.js App Router with separate rider and driver pages, API routes for ride operations that require PostGIS queries and Stripe charges, Supabase Realtime for live ride status and driver location updates, and a Stripe webhook for payment confirmation.

Final result

A ride-hailing platform with rider and driver interfaces, real-time GPS tracking on an interactive map, PostGIS-powered nearest-driver matching, distance-based fare calculation, Stripe payment processing, and a mutual rating system.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase + Realtime
StripePayment Processing
@vis.gl/react-google-mapsMap Display

Prerequisites

  • A V0 account (Premium recommended for the complexity of this build)
  • A Supabase project with PostGIS extension enabled (free tier works — connect via V0's Connect panel)
  • A Stripe account with test mode keys
  • A Google Maps API key with Maps JavaScript API enabled

Build steps

1

Set up the project, PostGIS, and ride-hailing schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Enable the PostGIS extension, then create the schema for riders, drivers with spatial columns, rides, and ratings.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a ride-hailing platform. First, enable PostGIS:
3// CREATE EXTENSION IF NOT EXISTS postgis;
4//
5// Create a Supabase schema with:
6// 1. riders: id (uuid PK), user_id (uuid FK UNIQUE), first_name (text), last_name (text), phone (text), payment_method_id (text), rating (numeric DEFAULT 5.0), created_at (timestamptz)
7// 2. drivers: id (uuid PK), user_id (uuid FK UNIQUE), first_name (text), last_name (text), phone (text), vehicle_make (text), vehicle_model (text), vehicle_plate (text), license_number (text), status (text CHECK offline/available/busy), current_location (geography(Point, 4326)), rating (numeric DEFAULT 5.0), created_at (timestamptz)
8// Add a spatial index: CREATE INDEX idx_drivers_location ON drivers USING GIST (current_location)
9// 3. rides: id (uuid PK), rider_id (uuid FK), driver_id (uuid FK), pickup_location (geography(Point, 4326)), dropoff_location (geography(Point, 4326)), pickup_address (text), dropoff_address (text), status (text CHECK requested/matching/accepted/arriving/in_progress/completed/cancelled), fare_estimate (integer), fare_final (integer), distance_meters (integer), duration_seconds (integer), requested_at (timestamptz), completed_at (timestamptz)
10// 4. ride_ratings: id (uuid PK), ride_id (uuid FK), from_user_id (uuid FK), to_user_id (uuid FK), rating (integer CHECK 1-5), comment (text), created_at (timestamptz)
11// Enable Supabase Realtime on rides and drivers tables.
12// RLS: riders see own rides, drivers see assigned rides, driver location updates only by self.
13// Generate SQL migration and TypeScript types.

Pro tip: Enable PostGIS BEFORE creating the schema — the geography(Point, 4326) column type requires the extension to exist. Run CREATE EXTENSION postgis in the Supabase SQL editor if V0 doesn't do it automatically.

Expected result: Supabase is connected with PostGIS enabled, all four tables created with a spatial index on drivers.current_location, and Realtime enabled on rides and drivers tables.

2

Build the rider interface with ride request flow

Create the rider page with a map for selecting pickup and dropoff locations, a ride request button, and live tracking of the assigned driver's location once a ride is accepted.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a rider page at app/ride/page.tsx.
3// Requirements:
4// - Interactive map using @vis.gl/react-google-maps showing the rider's current location
5// - Tap or search to set pickup and dropoff locations (markers on map)
6// - Card at bottom showing pickup/dropoff addresses, estimated fare, estimated time
7// - "Request Ride" Button that calls POST /api/rides/request
8// - After requesting: Skeleton loading state while matching
9// - Once matched: show driver Card with Avatar, name, vehicle info, rating Badge
10// - Live driver location on map via Supabase Realtime subscription on drivers table
11// - Ride status Badge (matching → accepted → arriving → in_progress → completed)
12// - "Cancel Ride" Button with AlertDialog confirmation (only before in_progress)
13// - On completion: rating Dialog with 1-5 stars and optional comment
14// - 'use client' for the map and Realtime listener
15// - Use shadcn/ui Card, Badge, Avatar, Button, AlertDialog, Skeleton, Toast, Sheet

Expected result: A rider page with an interactive map, pickup/dropoff selection, ride request flow, live driver tracking, and a post-ride rating dialog.

3

Create the driver matching API with PostGIS spatial queries

Build the API route that finds the nearest available drivers using PostGIS, creates a ride record, and atomically marks the matched driver as busy to prevent race conditions.

app/actions/rides.ts
1'use server'
2
3import { createClient } from '@/lib/supabase/server'
4
5export async function requestRide(formData: FormData) {
6 const supabase = await createClient()
7 const { data: { user } } = await supabase.auth.getUser()
8 if (!user) throw new Error('Must be logged in')
9
10 const pickupLat = parseFloat(formData.get('pickup_lat') as string)
11 const pickupLng = parseFloat(formData.get('pickup_lng') as string)
12 const dropoffLat = parseFloat(formData.get('dropoff_lat') as string)
13 const dropoffLng = parseFloat(formData.get('dropoff_lng') as string)
14
15 // Find nearest available driver within 5km and atomically set to busy
16 const { data: driver, error: matchError } = await supabase.rpc(
17 'match_nearest_driver',
18 {
19 p_pickup_lng: pickupLng,
20 p_pickup_lat: pickupLat,
21 p_radius_meters: 5000,
22 }
23 )
24
25 if (matchError || !driver) {
26 throw new Error('No drivers available nearby')
27 }
28
29 const { data: ride } = await supabase
30 .from('rides')
31 .insert({
32 rider_id: user.id,
33 driver_id: driver.id,
34 pickup_location: `POINT(${pickupLng} ${pickupLat})`,
35 dropoff_location: `POINT(${dropoffLng} ${dropoffLat})`,
36 pickup_address: formData.get('pickup_address') as string,
37 dropoff_address: formData.get('dropoff_address') as string,
38 fare_estimate: parseInt(formData.get('fare_estimate') as string),
39 status: 'accepted',
40 requested_at: new Date().toISOString(),
41 })
42 .select()
43 .single()
44
45 return ride
46}

Pro tip: The match_nearest_driver RPC function must run inside a transaction: SELECT ... FROM drivers WHERE status = 'available' AND ST_DWithin(current_location, ST_MakePoint($lng, $lat)::geography, $radius) ORDER BY ST_Distance(current_location, ST_MakePoint($lng, $lat)::geography) LIMIT 1 FOR UPDATE — then UPDATE drivers SET status = 'busy'. The FOR UPDATE lock prevents two concurrent requests from matching the same driver.

Expected result: A Server Action that finds the nearest available driver using PostGIS, creates a ride, and atomically locks the driver to prevent double-matching.

4

Build the driver interface and ride completion with Stripe

Create the driver page with incoming ride requests, accept/reject controls, navigation to pickup, ride completion, and Stripe payment processing on ride end.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a driver page at app/driver/page.tsx and a ride completion API.
3// Driver page requirements:
4// - Map showing current location and active ride route
5// - When offline: Toggle Switch to go online (sets status to 'available', starts location broadcasting)
6// - When available: waiting Card with Skeleton
7// - Incoming ride: Card showing rider Avatar, pickup address, estimated fare, distance
8// - Accept Button and Reject Button with 15-second auto-reject countdown
9// - During ride: navigation view with pickup/dropoff markers, status progression Buttons
10// - "Arrived at Pickup" → "Start Ride" → "Complete Ride"
11// - Supabase Realtime subscription on rides table filtered by driver_id
12// - Location update: broadcast driver position every 5 seconds via /api/rides/[id]/update-location
13//
14// Ride completion API at app/api/rides/[id]/complete/route.ts:
15// - Calculate final fare from distance_meters and duration_seconds
16// - Create Stripe PaymentIntent with rider's payment_method_id
17// - Update ride with fare_final and status 'completed'
18// - Return ride summary
19//
20// Stripe webhook at app/api/webhooks/stripe/route.ts:
21// - Verify signature using request.text() for raw body
22// - Handle payment_intent.succeeded — confirm ride payment
23//
24// Use shadcn/ui Card, Badge, Avatar, Button, Switch, AlertDialog, Toast, Skeleton

Expected result: A driver page with online/offline toggle, ride accept/reject, navigation controls, and an API route that calculates the final fare and charges via Stripe on ride completion.

Complete code

app/api/rides/[id]/complete/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import Stripe from 'stripe'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
10
11const BASE_FARE = 250 // $2.50
12const PER_KM = 150 // $1.50 per km
13const PER_MIN = 25 // $0.25 per minute
14
15export async function POST(
16 req: NextRequest,
17 { params }: { params: Promise<{ id: string }> }
18) {
19 const { id } = await params
20
21 const { data: ride } = await supabase
22 .from('rides')
23 .select('*, riders!inner(payment_method_id)')
24 .eq('id', id)
25 .eq('status', 'in_progress')
26 .single()
27
28 if (!ride) {
29 return NextResponse.json({ error: 'Ride not found' }, { status: 404 })
30 }
31
32 const distanceKm = (ride.distance_meters || 0) / 1000
33 const durationMin = (ride.duration_seconds || 0) / 60
34 const fareFinal = Math.round(BASE_FARE + distanceKm * PER_KM + durationMin * PER_MIN)
35
36 await stripe.paymentIntents.create({
37 amount: fareFinal,
38 currency: 'usd',
39 payment_method: ride.riders.payment_method_id,
40 confirm: true,
41 automatic_payment_methods: {
42 enabled: true,
43 allow_redirects: 'never',
44 },
45 metadata: { ride_id: id },
46 })
47
48 const { data: completed } = await supabase
49 .from('rides')
50 .update({
51 status: 'completed',
52 fare_final: fareFinal,
53 completed_at: new Date().toISOString(),
54 })
55 .eq('id', id)
56 .select()
57 .single()
58
59 return NextResponse.json(completed)
60}

Customization ideas

Add surge pricing

Calculate a surge multiplier based on the ratio of ride requests to available drivers in a geographic area, and apply it to fare estimates during high-demand periods.

Build ride sharing / carpooling

Match multiple riders heading in similar directions to the same driver, splitting fares based on individual distance traveled.

Add driver earnings dashboard

Build a dashboard showing daily/weekly earnings, trip count, average rating, and payout history using Recharts visualizations.

Implement ride scheduling

Let riders schedule rides in advance. Use a Supabase cron job to begin driver matching 10 minutes before the scheduled pickup time.

Common pitfalls

Pitfall: Not locking the driver row during matching, causing two riders to match the same driver

How to avoid: Use a Supabase RPC function with SELECT ... FOR UPDATE to lock the driver row, then UPDATE status to 'busy' inside the same transaction. This guarantees atomic matching.

Pitfall: Storing coordinates as separate latitude and longitude columns instead of PostGIS geography

How to avoid: Use the geography(Point, 4326) column type with a GIST index. PostGIS ST_DWithin and ST_Distance use the spatial index and calculate distances accurately on the Earth's surface.

Pitfall: Broadcasting driver location updates through the client instead of through an API route

How to avoid: Send location updates from the driver client to an API route (app/api/rides/[id]/update-location/route.ts) that validates and writes to Supabase. The Realtime subscription on the riders table picks up the change automatically.

Best practices

  • Enable PostGIS extension before creating schema — geography column types require it
  • Use ST_DWithin with a GIST spatial index for nearest-driver queries instead of calculating distances in application code
  • Lock driver rows with SELECT ... FOR UPDATE inside an RPC function to prevent concurrent matching race conditions
  • Use Supabase Realtime subscriptions on rides and drivers tables for live status and location updates
  • Store STRIPE_SECRET_KEY and SUPABASE_SERVICE_ROLE_KEY without NEXT_PUBLIC_ prefix in V0's Vars tab — they must stay server-only
  • Use request.text() (not request.json()) in the Stripe webhook handler to preserve the raw body for signature verification

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a ride-hailing platform with Next.js and Supabase with PostGIS. Write a PostgreSQL function called match_nearest_driver that takes pickup longitude, latitude, and radius in meters as parameters. It should find the nearest available driver within the radius using ST_DWithin, lock the row with FOR UPDATE, set the driver's status to 'busy', and return the driver record. Use a transaction to prevent race conditions. Include the CREATE FUNCTION statement.

Build Prompt

Create a ride status tracker component. Accept a ride object with status, driver info, and locations. Show a vertical stepper with stages: Requested → Matched → Driver Arriving → In Progress → Completed. Highlight the current stage with a colored Badge. Show driver Avatar, vehicle info, and ETA for active stages. Use shadcn/ui Card, Badge, Avatar, and Separator. Mark as 'use client' for Realtime updates.

Frequently asked questions

What V0 plan do I need for a ride-hailing platform?

V0 Premium ($20/month) is recommended because this build involves multiple complex pages (rider map, driver interface, APIs) that benefit from prompt queuing to generate sequentially.

How does the driver matching prevent two riders from getting the same driver?

A Supabase RPC function uses SELECT ... FOR UPDATE to lock the driver's database row during matching. Inside the same transaction, it sets the driver's status to 'busy'. This database-level lock guarantees that only one ride request can claim a driver, even under concurrent load.

Do I need a paid Google Maps API key?

Google Maps provides $200/month in free credits, which covers approximately 28,000 map loads. For development and small-scale testing, this is more than enough. Set NEXT_PUBLIC_GOOGLE_MAPS_KEY in V0's Vars tab.

How does real-time tracking work?

The driver client sends location updates every 5 seconds to an API route that writes to the drivers table. Supabase Realtime broadcasts the change to all subscribers. The rider client listens for updates on the matched driver's row and moves the marker on the map.

How do I deploy the ride-hailing platform?

Click Share then Publish to Production in V0. After the first deploy, register the Stripe webhook URL (https://yourdomain.vercel.app/api/webhooks/stripe) in the Stripe Dashboard. Set all env vars in V0's Vars tab: NEXT_PUBLIC_GOOGLE_MAPS_KEY (client), STRIPE_SECRET_KEY and SUPABASE_SERVICE_ROLE_KEY (server-only, no prefix).

Can RapidDev help build a custom ride-hailing platform?

Yes. RapidDev has built 600+ apps including real-time geospatial platforms with driver matching, surge pricing, and multi-region deployment. Book a free consultation to discuss your 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.