# How to Build Real estate listing with V0

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

## TL;DR

Build a Zillow-style real estate listing app with V0 using Next.js, Supabase, and PostGIS for geospatial search. Features property listings with photo galleries, map-based search with radius filtering, saved listings, and agent inquiry forms — all in about 1-2 hours.

## Before you start

- A V0 account (Premium recommended for multiple page types)
- A Supabase project with PostGIS extension enabled (free tier works)
- Sample property data or photos for testing
- Basic understanding of real estate listing concepts

## Step-by-step guide

### 1. Set up the project and listing schema with PostGIS

Open V0 and create a new project. Use the Connect panel to add Supabase. Enable PostGIS for geospatial queries and create the schema for listings, images, saved listings, and inquiries.

```
// Paste this prompt into V0's AI chat:
// Build a real estate listing app. Create a Supabase schema with PostGIS enabled:
// 1. listings: id (uuid PK), agent_id (uuid FK), title (text), description (text), address (text), city (text), state (text), zip (text), latitude (numeric), longitude (numeric), price (integer), bedrooms (integer), bathrooms (numeric), sqft (integer), property_type (text check house/condo/townhouse/land), status (text check active/pending/sold), features (text[]), created_at (timestamptz), updated_at (timestamptz)
// 2. listing_images: id (uuid PK), listing_id (uuid FK), image_url (text), position (integer), created_at (timestamptz)
// 3. saved_listings: id (uuid PK), user_id (uuid FK), listing_id (uuid FK), created_at (timestamptz) with unique constraint on (user_id, listing_id)
// 4. inquiries: id (uuid PK), listing_id (uuid FK), user_id (uuid FK nullable), name (text), email (text), message (text), created_at (timestamptz)
// Enable PostGIS extension. Create a spatial index on listings (latitude, longitude).
// RLS: anyone can SELECT active listings, only agents can INSERT/UPDATE their own.
// Generate SQL migration and TypeScript types.
```

**Expected result:** Supabase is connected with PostGIS enabled. All four tables are created with a spatial index on listing coordinates. RLS allows public read access for active listings.

### 2. Build the search page with filters and listing cards

Create the main search page with a filter sidebar and a grid of listing cards. Filters include price range, bedrooms, bathrooms, and property type. On mobile, filters collapse into a Sheet.

```
// Paste this prompt into V0's AI chat:
// Build a real estate search page at app/page.tsx.
// Requirements:
// - Two-column layout: filter sidebar (left, 280px) and listing grid (right)
// - Filter sidebar with:
//   - Input for location/city search
//   - Slider for price range (min/max)
//   - Select for bedrooms (Any, 1+, 2+, 3+, 4+)
//   - Select for bathrooms (Any, 1+, 2+, 3+)
//   - Select for property type (All, House, Condo, Townhouse, Land)
//   - Badge chips showing active filters with X to remove
//   - On mobile: filters collapse into a Sheet triggered by a filter Button
// - Listing grid: shadcn/ui Card for each listing showing:
//   - Image (first listing_image), Skeleton while loading
//   - Price formatted as currency, bedrooms/bathrooms/sqft
//   - Badge for status (active=green, pending=yellow, sold=red)
//   - Heart icon Button for save/unsave
//   - Card is clickable → navigates to /listings/[id]
// - Filters update URL searchParams for shareable filtered URLs
// - Server Components for initial data, 'use client' for filter interactions
// - Use Supabase .textSearch() for location search
```

> Pro tip: Use V0's Design Mode (Option+D) to visually adjust the listing card grid layout, image aspect ratios, and search bar positioning for free.

**Expected result:** A search page with a filter sidebar (Sheet on mobile), listing card grid with images and price, and URL-synced filters for shareable search results.

### 3. Create the listing detail page with image gallery

Build the individual listing page showing the full property details, a Carousel image gallery, features list, and an inquiry form for contacting the agent.

```
// Paste this prompt into V0's AI chat:
// Build a listing detail page at app/listings/[id]/page.tsx.
// Requirements:
// - Fetch listing with images and agent info from Supabase
// - Top section: shadcn/ui Carousel for image gallery with thumbnails below
// - Use Skeleton for image loading states
// - Detail section with two columns:
//   - Left: price (large), address, bedrooms/bathrooms/sqft row, description paragraph
//   - Right: Agent Card with Avatar and name, inquiry form with Input for name/email, Textarea for message, Button to submit
// - Features section: list of features as Badge components in a flex wrap
// - Status Badge at top (active/pending/sold)
// - Heart Button to save/unsave listing (Server Action toggleFavorite)
// - Server Action submitInquiry() for the contact form
// - Use Server Components for SEO-optimized data fetching
// - Generate proper metadata for the listing title and description
```

**Expected result:** A listing detail page with an image Carousel, property details with formatted price and features, an agent contact form, and a save/unsave heart button.

### 4. Add geospatial search with PostGIS

Build an API route that uses PostGIS to find properties within a radius of a given location. This enables "properties near me" and "properties within 5 miles of downtown" searches.

```
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!
)

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url)
  const lat = parseFloat(searchParams.get('lat') ?? '0')
  const lng = parseFloat(searchParams.get('lng') ?? '0')
  const radiusMiles = parseFloat(searchParams.get('radius') ?? '10')
  const radiusMeters = radiusMiles * 1609.34
  const minPrice = parseInt(searchParams.get('min_price') ?? '0')
  const maxPrice = parseInt(searchParams.get('max_price') ?? '999999999')
  const bedrooms = parseInt(searchParams.get('bedrooms') ?? '0')

  const { data, error } = await supabase.rpc('search_listings_nearby', {
    search_lat: lat,
    search_lng: lng,
    radius_meters: radiusMeters,
    min_price: minPrice,
    max_price: maxPrice,
    min_bedrooms: bedrooms,
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ listings: data })
}
```

> Pro tip: Create the search_listings_nearby Supabase RPC function with: SELECT *, ST_Distance(ST_MakePoint(longitude, latitude)::geography, ST_MakePoint(search_lng, search_lat)::geography) as distance_meters FROM listings WHERE ST_DWithin(...) and price filters, ORDER BY distance_meters.

**Expected result:** GET /api/listings/search?lat=40.7&lng=-74.0&radius=5 returns listings within 5 miles of the coordinates, sorted by distance, with price and bedroom filters applied.

### 5. Build the listing creation form with image uploads

Create a form where agents can post new listings with property details and upload multiple images to Supabase Storage.

```
// Paste this prompt into V0's AI chat:
// Build a listing creation page at app/listings/new/page.tsx.
// Requirements:
// - Protected by auth (only agents can access)
// - Form sections using shadcn/ui Card:
//   - Basic Info: Input for title, Textarea for description, Select for property_type
//   - Location: Input for address, city, state, zip (auto-geocode to lat/lng)
//   - Details: Input for price, bedrooms, bathrooms, sqft
//   - Features: ToggleGroup for common features (pool, garage, fireplace, etc) plus custom Input
//   - Images: drag-and-drop upload zone for multiple images, preview thumbnails, reorder by dragging
// - Images upload to Supabase Storage public bucket 'listing-images'
// - On submit: Server Action creates listing, uploads images, creates listing_images records
// - Show Progress during upload
// - After creation, redirect to the new listing page
// - 'use client' for the interactive form with file uploads
```

**Expected result:** A multi-section listing creation form with drag-and-drop image uploads to Supabase Storage. Submitting creates the listing and redirects to its detail page.

## Complete code example

File: `app/api/listings/search/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!
)

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url)
  const lat = parseFloat(searchParams.get('lat') ?? '0')
  const lng = parseFloat(searchParams.get('lng') ?? '0')
  const radiusMiles = parseFloat(searchParams.get('radius') ?? '10')
  const minPrice = parseInt(searchParams.get('min_price') ?? '0')
  const maxPrice = parseInt(searchParams.get('max_price') ?? '999999999')
  const bedrooms = parseInt(searchParams.get('bedrooms') ?? '0')
  const propertyType = searchParams.get('type')

  let query = supabase
    .from('listings')
    .select('*, listing_images(image_url, position)')
    .eq('status', 'active')
    .gte('price', minPrice)
    .lte('price', maxPrice)
    .gte('bedrooms', bedrooms)

  if (propertyType && propertyType !== 'all') {
    query = query.eq('property_type', propertyType)
  }

  if (lat && lng) {
    const { data, error } = await supabase.rpc(
      'search_listings_nearby',
      {
        search_lat: lat,
        search_lng: lng,
        radius_meters: radiusMiles * 1609.34,
        min_price: minPrice,
        max_price: maxPrice,
        min_bedrooms: bedrooms,
      }
    )
    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 })
    }
    return NextResponse.json({ listings: data })
  }

  const { data, error } = await query
    .order('created_at', { ascending: false })
    .limit(50)

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
  return NextResponse.json({ listings: data })
}
```

## Common mistakes

- **Not enabling PostGIS extension before running spatial queries** — Supabase does not include PostGIS by default. Without it, functions like ST_DWithin and ST_Distance do not exist and queries fail. Fix: Enable PostGIS in Supabase Dashboard: Database > Extensions > search PostGIS > Enable. Then create the spatial index on listing coordinates.
- **Uploading listing images to a private Supabase Storage bucket** — Private buckets require signed URLs that expire. Listing images need to be publicly accessible for SEO and social sharing. Fix: Create a public bucket named listing-images in Supabase Storage. Set a bucket policy allowing public SELECT. Store the public URL in listing_images.image_url.
- **Fetching all listings on the search page without pagination** — Hundreds or thousands of listings loaded at once cause slow page loads and excessive Supabase bandwidth usage. Fix: Use Supabase .range() for pagination with a limit of 20-50 listings per page. Add a Load More button or infinite scroll.

## Best practices

- Use Server Components for listing detail pages to optimize SEO — search engines index the server-rendered HTML
- Store listing images in a Supabase Storage public bucket so URLs are permanently accessible without signing
- Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars tab (both are publishable keys, so NEXT_PUBLIC_ is correct)
- Use V0's Design Mode to visually adjust the listing card grid layout and image aspect ratios for free
- Create a spatial index on listing coordinates for fast geospatial queries at scale
- Use URL searchParams for filter state so filtered search results are shareable and bookmarkable

## Frequently asked questions

### Do I need a paid V0 plan for a real estate listing app?

V0 Free works for the basic build, but Premium ($20/month) is recommended because the app has multiple page types (search, listing detail, creation form) that benefit from prompt queuing.

### How does the geospatial search work?

PostGIS is a PostgreSQL extension that adds geographic functions. The search API uses ST_DWithin to find listings within a radius and ST_Distance to sort by proximity. Enable PostGIS in your Supabase Dashboard under Extensions.

### Can I add an interactive map with property pins?

Yes. Add react-map-gl or @vis.gl/react-google-maps to display listings as map markers. Set NEXT_PUBLIC_GOOGLE_MAPS_KEY or NEXT_PUBLIC_MAPBOX_TOKEN in the Vars tab (publishable keys, so NEXT_PUBLIC_ is correct).

### How do I handle property image uploads?

Images upload to a Supabase Storage public bucket. The public URL is stored in the listing_images table. Use the Supabase Storage SDK in a Server Action to handle the upload and return the public URL.

### How do I deploy the listing app?

Click Share then Publish to Production in V0. Your listing pages are server-rendered for SEO. Set Supabase credentials via the Connect panel and any map API keys in the Vars tab.

### Can RapidDev help build a custom real estate platform?

Yes. RapidDev has built 600+ apps including real estate platforms with MLS integrations, mortgage calculators, and agent management systems. Book a free consultation to discuss your requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/real-estate-listing
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/real-estate-listing
