Build a food delivery backend with V0 using Next.js, Supabase with Realtime for live order tracking, Stripe for payments, and PostGIS for driver proximity matching. You'll create restaurant menus, cart checkout, real-time order status updates, and driver assignment — all in about 2-4 hours without touching a terminal.
What you're building
Food delivery platforms need multiple connected interfaces: customers browsing menus and placing orders, restaurants managing incoming orders, and drivers accepting deliveries. Real-time status updates keep everyone informed throughout the order lifecycle.
V0 generates each interface from prompts. Use prompt queuing to build the customer flow, restaurant dashboard, and driver interface separately. Supabase Realtime provides instant status updates, and PostGIS enables proximity-based driver assignment.
The architecture uses Next.js App Router with Server Components for restaurant listings, client components for real-time order tracking, API routes for Stripe payments and order status updates, Supabase Realtime for live status subscriptions, and PostGIS for spatial queries.
Final result
A food delivery backend with restaurant menus, cart checkout, real-time order tracking, driver assignment, and three role-based dashboards.
Tech stack
Prerequisites
- A V0 account (Premium plan for the multi-dashboard build)
- A Supabase project with PostGIS extension enabled (free tier works)
- A Stripe account (test mode — add via Vercel Marketplace)
- Understanding of food delivery flow: order, pay, prepare, deliver
Build steps
Set up the database schema with PostGIS
Create the schema for restaurants, menu items, orders, drivers, and status logs. Enable PostGIS in Supabase for location-based driver assignment.
1// Paste this prompt into V0's AI chat:2// Build a food delivery backend. Create a Supabase schema with:3// 1. restaurants: id (uuid PK), owner_id (uuid FK to auth.users), name (text), description (text), address (text), lat (decimal), lng (decimal), cuisine_type (text), is_active (boolean default true), avg_prep_minutes (int), created_at (timestamptz)4// 2. menu_items: id (uuid PK), restaurant_id (uuid FK), name (text), description (text), price_cents (int), category (text), image_url (text), is_available (boolean default true)5// 3. orders: id (uuid PK), customer_id (uuid FK), restaurant_id (uuid FK), driver_id (uuid FK nullable), status (text default 'pending' check in 'pending','confirmed','preparing','ready','picked_up','delivering','delivered','cancelled'), items_json (jsonb), subtotal_cents (int), delivery_fee_cents (int), total_cents (int), delivery_address (text), delivery_lat (decimal), delivery_lng (decimal), stripe_payment_intent_id (text), created_at (timestamptz), updated_at (timestamptz)6// 4. drivers: id (uuid PK), user_id (uuid FK), is_available (boolean default true), current_lat (decimal), current_lng (decimal), vehicle_type (text), updated_at (timestamptz)7// 5. order_status_log: id (uuid PK), order_id (uuid FK), status (text), changed_by (uuid FK), created_at (timestamptz)8// Enable PostGIS extension. Enable Realtime on orders table.9// Add RLS for role-based access.Pro tip: Enable PostGIS in Supabase Dashboard under Database > Extensions before creating location queries. This gives you ST_DWithin and other spatial functions.
Expected result: Database schema created with PostGIS enabled and Realtime configured on the orders table.
Build the customer ordering flow
Create restaurant browsing, menu selection, cart, and Stripe checkout. The customer sees real-time order status after payment.
1// Paste this prompt into V0's AI chat:2// Build the customer ordering flow:3// 1. app/page.tsx — restaurant listing with Card components, cuisine Select filter, and distance sort4// 2. app/restaurant/[id]/page.tsx — menu with category Tabs, menu item Cards with "Add to Cart" Button, cart summary in Sheet sidebar5// 3. app/checkout/page.tsx — 'use client' cart review with item list, subtotal, delivery fee, total, and "Pay" Button that POSTs to /api/orders6// 4. app/orders/[id]/page.tsx — 'use client' real-time order tracking:7// - Subscribe to Supabase Realtime .on('postgres_changes', { event: 'UPDATE', filter: `id=eq.${orderId}` })8// - Show status Stepper: Pending > Confirmed > Preparing > Ready > Picked Up > Delivering > Delivered9// - Estimated delivery time, driver info when assigned10// API route: app/api/orders/route.ts — POST validates items, calculates totals, creates Stripe PaymentIntent, inserts order11// Use shadcn/ui Card, Badge, Sheet, Stepper patternExpected result: Customers can browse restaurants, add items to cart, pay via Stripe, and track their order status in real-time.
Create the order status update API with Realtime broadcast
Build the API route that updates order status, logs the change, and triggers Supabase Realtime notifications to all connected clients.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89const validTransitions: Record<string, string[]> = {10 pending: ['confirmed', 'cancelled'],11 confirmed: ['preparing', 'cancelled'],12 preparing: ['ready'],13 ready: ['picked_up'],14 picked_up: ['delivering'],15 delivering: ['delivered'],16}1718export async function PATCH(19 req: NextRequest,20 { params }: { params: Promise<{ id: string }> }21) {22 const { id } = await params23 const { status, changedBy } = await req.json()2425 const { data: order } = await supabase26 .from('orders')27 .select('status')28 .eq('id', id)29 .single()3031 if (!order) {32 return NextResponse.json({ error: 'Order not found' }, { status: 404 })33 }3435 const allowed = validTransitions[order.status]36 if (!allowed?.includes(status)) {37 return NextResponse.json(38 { error: `Cannot transition from ${order.status} to ${status}` },39 { status: 400 }40 )41 }4243 await supabase44 .from('orders')45 .update({ status, updated_at: new Date().toISOString() })46 .eq('id', id)4748 await supabase.from('order_status_log').insert({49 order_id: id,50 status,51 changed_by: changedBy,52 })5354 return NextResponse.json({ success: true })55}Pro tip: Use prompt queuing to build the customer flow, restaurant dashboard, and driver interface as three separate prompt sequences.
Expected result: Status updates are validated against allowed transitions, logged, and broadcast via Supabase Realtime to connected clients.
Build the restaurant dashboard
Create the restaurant owner's interface for managing incoming orders, confirming them, updating preparation status, and marking orders as ready for pickup.
1// Paste this prompt into V0's AI chat:2// Build the restaurant dashboard at app/restaurant/dashboard/page.tsx.3// Requirements:4// - 'use client' page that subscribes to orders Realtime for this restaurant5// - Three columns (Kanban-style): New Orders, Preparing, Ready for Pickup6// - Each order as a Card with: order ID, items list, total, customer address, time since ordered7// - New Orders: "Confirm" Button (transitions pending > confirmed > preparing)8// - Preparing: "Mark Ready" Button (transitions to ready)9// - Audio notification sound when new order arrives10// - Stats bar at top: total orders today, average prep time, revenue today11// - Use Badge for order status, Card for order items, Button for actionsExpected result: Restaurant owners see orders flowing through three columns. New orders trigger audio alerts. Status buttons move orders through the pipeline.
Build the driver interface with proximity assignment
Create the driver dashboard and the nearest-driver assignment API route using PostGIS spatial queries.
1// Paste this prompt into V0's AI chat:2// Build two things:3// 1. app/driver/page.tsx — driver dashboard ('use client'):4// - Toggle Switch: Available / Unavailable (updates drivers.is_available)5// - Subscribe to orders Realtime filtered by driver_id = current user6// - Available orders list (status = 'ready', no driver assigned): Card with restaurant name, pickup address, delivery address, payment amount7// - "Accept" Button that assigns the driver to the order8// - Active delivery Card (when assigned): order details + status action Buttons:9// - "Picked Up" (sets status to picked_up)10// - "Delivered" (sets status to delivered)11// - Delivery history Table below12// 2. app/api/drivers/assign/route.ts — POST that finds the nearest available driver:13// - Query: SELECT * FROM drivers WHERE is_available = true ORDER BY point(current_lng, current_lat) <-> point($lng, $lat) LIMIT 114// - Update the order's driver_id and set driver is_available = false15// - Return the assigned driver infoExpected result: Drivers see available orders, accept deliveries, and update status. The assignment API finds the nearest driver using PostGIS.
Handle Stripe webhook for payment confirmation
Build the webhook handler that confirms payment and triggers the order pipeline.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const body = await req.text()13 const sig = req.headers.get('stripe-signature')!1415 let event: Stripe.Event16 try {17 event = stripe.webhooks.constructEvent(18 body, sig, process.env.STRIPE_WEBHOOK_SECRET!19 )20 } catch {21 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })22 }2324 if (event.type === 'payment_intent.succeeded') {25 const pi = event.data.object as Stripe.PaymentIntent26 const orderId = pi.metadata.orderId2728 await supabase29 .from('orders')30 .update({ status: 'confirmed' })31 .eq('id', orderId)3233 await supabase.from('order_status_log').insert({34 order_id: orderId,35 status: 'confirmed',36 changed_by: pi.metadata.customerId,37 })38 }3940 return NextResponse.json({ received: true })41}Expected result: Payment confirmation triggers order status change to 'confirmed', which restaurant dashboard sees via Realtime.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89const validTransitions: Record<string, string[]> = {10 pending: ['confirmed', 'cancelled'],11 confirmed: ['preparing', 'cancelled'],12 preparing: ['ready'],13 ready: ['picked_up'],14 picked_up: ['delivering'],15 delivering: ['delivered'],16}1718export async function PATCH(19 req: NextRequest,20 { params }: { params: Promise<{ id: string }> }21) {22 const { id } = await params23 const { status, changedBy } = await req.json()2425 const { data: order } = await supabase26 .from('orders')27 .select('status')28 .eq('id', id)29 .single()3031 if (!order) {32 return NextResponse.json({ error: 'Not found' }, { status: 404 })33 }3435 if (!validTransitions[order.status]?.includes(status)) {36 return NextResponse.json(37 { error: `Invalid transition: ${order.status} to ${status}` },38 { status: 400 }39 )40 }4142 await supabase43 .from('orders')44 .update({ status, updated_at: new Date().toISOString() })45 .eq('id', id)4647 await supabase.from('order_status_log').insert({48 order_id: id,49 status,50 changed_by: changedBy,51 })5253 return NextResponse.json({ success: true })54}Customization ideas
Add live driver location tracking
Update driver coordinates via Supabase Realtime Broadcast and show a live map pin on the customer's order tracking page.
Add restaurant ratings and reviews
Let customers rate completed orders and leave reviews, displayed on restaurant listing cards.
Add promo codes and discounts
Create a promo_codes table and apply percentage or fixed discounts at checkout with validation in the order API route.
Add estimated delivery time
Calculate estimated delivery based on restaurant prep time, distance to customer, and current driver availability.
Common pitfalls
Pitfall: Not validating order status transitions server-side
How to avoid: Define a validTransitions map and check every status update against it before applying the change.
Pitfall: Using request.json() in the Stripe webhook handler
How to avoid: Use await req.text() and pass the raw body to stripe.webhooks.constructEvent().
Pitfall: Not enabling Realtime replication on the orders table
How to avoid: Enable Realtime replication on the orders table in Supabase Dashboard under Database > Replication.
Best practices
- Validate order status transitions with a server-side state machine to prevent invalid jumps.
- Use Supabase Realtime postgres_changes for live order tracking on the customer page.
- Enable PostGIS extension for proximity-based driver assignment with spatial queries.
- Use prompt queuing to build customer, restaurant, and driver interfaces as separate prompt sequences.
- Use request.text() in the Stripe webhook handler for proper signature verification.
- Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and SUPABASE_SERVICE_ROLE_KEY in Vars without NEXT_PUBLIC_ prefix.
- Enable Realtime replication on the orders table in Supabase Dashboard.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a food delivery backend with Next.js and Supabase. I need real-time order tracking where customers see status updates instantly. Show me how to subscribe to Supabase Realtime postgres_changes on the orders table filtered by order ID, update the UI on status change, and properly clean up the subscription on component unmount.
Build the nearest-driver assignment API for a food delivery backend. Create an API route that queries Supabase with PostGIS to find the nearest available driver to a given latitude/longitude. Use the point <-> operator for distance sorting. Assign the driver to the order and set their availability to false. Handle the case where no drivers are available.
Frequently asked questions
How does real-time order tracking work?
The customer's order page subscribes to Supabase Realtime postgres_changes filtered by the order ID. When the restaurant or driver updates the status via the API, the status change is broadcast to all connected clients instantly.
How does driver assignment work?
When an order is ready for pickup, the assignment API queries the drivers table using PostGIS to find the nearest available driver. It assigns them to the order and sets their availability to false.
Do I need PostGIS for driver matching?
PostGIS enables efficient spatial queries. Enable it in Supabase Dashboard under Database > Extensions. For a simpler approach, you can calculate distances in JavaScript, but PostGIS is much faster for production use.
What V0 plan do I need?
Premium ($20/month) is recommended. The food delivery backend has three separate interfaces (customer, restaurant, driver) that require many prompt iterations.
How do I deploy?
Publish via V0's Share menu. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and SUPABASE_SERVICE_ROLE_KEY in the Vars tab. Enable Realtime on the orders table. Register the Stripe webhook URL.
Can RapidDev help build a custom food delivery platform?
Yes. RapidDev has built 600+ apps including logistics platforms with real-time tracking, route optimization, and payment processing. Book a free consultation to discuss your delivery app concept.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation