# How to Build Food delivery backend with V0

- Tool: v0
- Difficulty: Advanced
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

## Step-by-step guide

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

```
// Paste this prompt into V0's AI chat:
// Build a food delivery backend. Create a Supabase schema with:
// 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)
// 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)
// 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)
// 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)
// 5. order_status_log: id (uuid PK), order_id (uuid FK), status (text), changed_by (uuid FK), created_at (timestamptz)
// Enable PostGIS extension. Enable Realtime on orders table.
// 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.

### 2. Build the customer ordering flow

Create restaurant browsing, menu selection, cart, and Stripe checkout. The customer sees real-time order status after payment.

```
// Paste this prompt into V0's AI chat:
// Build the customer ordering flow:
// 1. app/page.tsx — restaurant listing with Card components, cuisine Select filter, and distance sort
// 2. app/restaurant/[id]/page.tsx — menu with category Tabs, menu item Cards with "Add to Cart" Button, cart summary in Sheet sidebar
// 3. app/checkout/page.tsx — 'use client' cart review with item list, subtotal, delivery fee, total, and "Pay" Button that POSTs to /api/orders
// 4. app/orders/[id]/page.tsx — 'use client' real-time order tracking:
//    - Subscribe to Supabase Realtime .on('postgres_changes', { event: 'UPDATE', filter: `id=eq.${orderId}` })
//    - Show status Stepper: Pending > Confirmed > Preparing > Ready > Picked Up > Delivering > Delivered
//    - Estimated delivery time, driver info when assigned
// API route: app/api/orders/route.ts — POST validates items, calculates totals, creates Stripe PaymentIntent, inserts order
// Use shadcn/ui Card, Badge, Sheet, Stepper pattern
```

**Expected result:** Customers can browse restaurants, add items to cart, pay via Stripe, and track their order status in real-time.

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

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const validTransitions: Record<string, string[]> = {
  pending: ['confirmed', 'cancelled'],
  confirmed: ['preparing', 'cancelled'],
  preparing: ['ready'],
  ready: ['picked_up'],
  picked_up: ['delivering'],
  delivering: ['delivered'],
}

export async function PATCH(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const { status, changedBy } = await req.json()

  const { data: order } = await supabase
    .from('orders')
    .select('status')
    .eq('id', id)
    .single()

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

  const allowed = validTransitions[order.status]
  if (!allowed?.includes(status)) {
    return NextResponse.json(
      { error: `Cannot transition from ${order.status} to ${status}` },
      { status: 400 }
    )
  }

  await supabase
    .from('orders')
    .update({ status, updated_at: new Date().toISOString() })
    .eq('id', id)

  await supabase.from('order_status_log').insert({
    order_id: id,
    status,
    changed_by: changedBy,
  })

  return NextResponse.json({ success: true })
}
```

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

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

```
// Paste this prompt into V0's AI chat:
// Build the restaurant dashboard at app/restaurant/dashboard/page.tsx.
// Requirements:
// - 'use client' page that subscribes to orders Realtime for this restaurant
// - Three columns (Kanban-style): New Orders, Preparing, Ready for Pickup
// - Each order as a Card with: order ID, items list, total, customer address, time since ordered
// - New Orders: "Confirm" Button (transitions pending > confirmed > preparing)
// - Preparing: "Mark Ready" Button (transitions to ready)
// - Audio notification sound when new order arrives
// - Stats bar at top: total orders today, average prep time, revenue today
// - Use Badge for order status, Card for order items, Button for actions
```

**Expected result:** Restaurant owners see orders flowing through three columns. New orders trigger audio alerts. Status buttons move orders through the pipeline.

### 5. Build the driver interface with proximity assignment

Create the driver dashboard and the nearest-driver assignment API route using PostGIS spatial queries.

```
// Paste this prompt into V0's AI chat:
// Build two things:
// 1. app/driver/page.tsx — driver dashboard ('use client'):
//    - Toggle Switch: Available / Unavailable (updates drivers.is_available)
//    - Subscribe to orders Realtime filtered by driver_id = current user
//    - Available orders list (status = 'ready', no driver assigned): Card with restaurant name, pickup address, delivery address, payment amount
//    - "Accept" Button that assigns the driver to the order
//    - Active delivery Card (when assigned): order details + status action Buttons:
//      - "Picked Up" (sets status to picked_up)
//      - "Delivered" (sets status to delivered)
//    - Delivery history Table below
// 2. app/api/drivers/assign/route.ts — POST that finds the nearest available driver:
//    - Query: SELECT * FROM drivers WHERE is_available = true ORDER BY point(current_lng, current_lat) <-> point($lng, $lat) LIMIT 1
//    - Update the order's driver_id and set driver is_available = false
//    - Return the assigned driver info
```

**Expected result:** Drivers see available orders, accept deliveries, and update status. The assignment API finds the nearest driver using PostGIS.

### 6. Handle Stripe webhook for payment confirmation

Build the webhook handler that confirms payment and triggers the order pipeline.

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

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const body = await req.text()
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(
      body, sig, process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  if (event.type === 'payment_intent.succeeded') {
    const pi = event.data.object as Stripe.PaymentIntent
    const orderId = pi.metadata.orderId

    await supabase
      .from('orders')
      .update({ status: 'confirmed' })
      .eq('id', orderId)

    await supabase.from('order_status_log').insert({
      order_id: orderId,
      status: 'confirmed',
      changed_by: pi.metadata.customerId,
    })
  }

  return NextResponse.json({ received: true })
}
```

**Expected result:** Payment confirmation triggers order status change to 'confirmed', which restaurant dashboard sees via Realtime.

## Complete code example

File: `app/api/orders/[id]/status/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const validTransitions: Record<string, string[]> = {
  pending: ['confirmed', 'cancelled'],
  confirmed: ['preparing', 'cancelled'],
  preparing: ['ready'],
  ready: ['picked_up'],
  picked_up: ['delivering'],
  delivering: ['delivered'],
}

export async function PATCH(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const { status, changedBy } = await req.json()

  const { data: order } = await supabase
    .from('orders')
    .select('status')
    .eq('id', id)
    .single()

  if (!order) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }

  if (!validTransitions[order.status]?.includes(status)) {
    return NextResponse.json(
      { error: `Invalid transition: ${order.status} to ${status}` },
      { status: 400 }
    )
  }

  await supabase
    .from('orders')
    .update({ status, updated_at: new Date().toISOString() })
    .eq('id', id)

  await supabase.from('order_status_log').insert({
    order_id: id,
    status,
    changed_by: changedBy,
  })

  return NextResponse.json({ success: true })
}
```

## Common mistakes

- **Not validating order status transitions server-side** — Without a state machine, API calls could skip steps (e.g., jumping from pending directly to delivered), breaking the order pipeline. Fix: Define a validTransitions map and check every status update against it before applying the change.
- **Using request.json() in the Stripe webhook handler** — Stripe signature verification requires the raw body. Parsing as JSON first causes signature mismatch. Fix: Use await req.text() and pass the raw body to stripe.webhooks.constructEvent().
- **Not enabling Realtime replication on the orders table** — Without Realtime enabled, client subscriptions receive no events and the order tracking page appears frozen. Fix: 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.

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

---

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