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.
What you're building
Real estate apps need to display properties beautifully, search efficiently by location and features, and connect buyers with agents. Whether you are building a local MLS alternative or a niche property marketplace, the core features are the same — listings, search, photos, and inquiries.
V0 generates the listing pages, search interface, and image gallery from prompts. Supabase with PostGIS handles geospatial queries like finding properties within 5 miles of a location. Supabase Storage handles property photo uploads.
The architecture uses Next.js App Router with Server Components for listing pages (great for SEO), an API route for complex geospatial search queries, Server Actions for saving listings and submitting inquiries, and Supabase Storage for property images with public bucket access.
Final result
A searchable real estate listing platform with property cards, photo galleries, map-based geospatial search, saved listings, agent inquiry forms, and a listing creation interface for agents.
Tech stack
Prerequisites
- 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
Build steps
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.
1// Paste this prompt into V0's AI chat:2// Build a real estate listing app. Create a Supabase schema with PostGIS enabled:3// 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)4// 2. listing_images: id (uuid PK), listing_id (uuid FK), image_url (text), position (integer), created_at (timestamptz)5// 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)6// 4. inquiries: id (uuid PK), listing_id (uuid FK), user_id (uuid FK nullable), name (text), email (text), message (text), created_at (timestamptz)7// Enable PostGIS extension. Create a spatial index on listings (latitude, longitude).8// RLS: anyone can SELECT active listings, only agents can INSERT/UPDATE their own.9// 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.
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.
1// Paste this prompt into V0's AI chat:2// Build a real estate search page at app/page.tsx.3// Requirements:4// - Two-column layout: filter sidebar (left, 280px) and listing grid (right)5// - Filter sidebar with:6// - Input for location/city search7// - Slider for price range (min/max)8// - Select for bedrooms (Any, 1+, 2+, 3+, 4+)9// - Select for bathrooms (Any, 1+, 2+, 3+)10// - Select for property type (All, House, Condo, Townhouse, Land)11// - Badge chips showing active filters with X to remove12// - On mobile: filters collapse into a Sheet triggered by a filter Button13// - Listing grid: shadcn/ui Card for each listing showing:14// - Image (first listing_image), Skeleton while loading15// - Price formatted as currency, bedrooms/bathrooms/sqft16// - Badge for status (active=green, pending=yellow, sold=red)17// - Heart icon Button for save/unsave18// - Card is clickable → navigates to /listings/[id]19// - Filters update URL searchParams for shareable filtered URLs20// - Server Components for initial data, 'use client' for filter interactions21// - Use Supabase .textSearch() for location searchPro 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.
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.
1// Paste this prompt into V0's AI chat:2// Build a listing detail page at app/listings/[id]/page.tsx.3// Requirements:4// - Fetch listing with images and agent info from Supabase5// - Top section: shadcn/ui Carousel for image gallery with thumbnails below6// - Use Skeleton for image loading states7// - Detail section with two columns:8// - Left: price (large), address, bedrooms/bathrooms/sqft row, description paragraph9// - Right: Agent Card with Avatar and name, inquiry form with Input for name/email, Textarea for message, Button to submit10// - Features section: list of features as Badge components in a flex wrap11// - Status Badge at top (active/pending/sold)12// - Heart Button to save/unsave listing (Server Action toggleFavorite)13// - Server Action submitInquiry() for the contact form14// - Use Server Components for SEO-optimized data fetching15// - Generate proper metadata for the listing title and descriptionExpected 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.
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.
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)89export async function GET(req: NextRequest) {10 const { searchParams } = new URL(req.url)11 const lat = parseFloat(searchParams.get('lat') ?? '0')12 const lng = parseFloat(searchParams.get('lng') ?? '0')13 const radiusMiles = parseFloat(searchParams.get('radius') ?? '10')14 const radiusMeters = radiusMiles * 1609.3415 const minPrice = parseInt(searchParams.get('min_price') ?? '0')16 const maxPrice = parseInt(searchParams.get('max_price') ?? '999999999')17 const bedrooms = parseInt(searchParams.get('bedrooms') ?? '0')1819 const { data, error } = await supabase.rpc('search_listings_nearby', {20 search_lat: lat,21 search_lng: lng,22 radius_meters: radiusMeters,23 min_price: minPrice,24 max_price: maxPrice,25 min_bedrooms: bedrooms,26 })2728 if (error) {29 return NextResponse.json({ error: error.message }, { status: 500 })30 }3132 return NextResponse.json({ listings: data })33}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.
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.
1// Paste this prompt into V0's AI chat:2// Build a listing creation page at app/listings/new/page.tsx.3// Requirements:4// - Protected by auth (only agents can access)5// - Form sections using shadcn/ui Card:6// - Basic Info: Input for title, Textarea for description, Select for property_type7// - Location: Input for address, city, state, zip (auto-geocode to lat/lng)8// - Details: Input for price, bedrooms, bathrooms, sqft9// - Features: ToggleGroup for common features (pool, garage, fireplace, etc) plus custom Input10// - Images: drag-and-drop upload zone for multiple images, preview thumbnails, reorder by dragging11// - Images upload to Supabase Storage public bucket 'listing-images'12// - On submit: Server Action creates listing, uploads images, creates listing_images records13// - Show Progress during upload14// - After creation, redirect to the new listing page15// - 'use client' for the interactive form with file uploadsExpected 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
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)89export async function GET(req: NextRequest) {10 const { searchParams } = new URL(req.url)11 const lat = parseFloat(searchParams.get('lat') ?? '0')12 const lng = parseFloat(searchParams.get('lng') ?? '0')13 const radiusMiles = parseFloat(searchParams.get('radius') ?? '10')14 const minPrice = parseInt(searchParams.get('min_price') ?? '0')15 const maxPrice = parseInt(searchParams.get('max_price') ?? '999999999')16 const bedrooms = parseInt(searchParams.get('bedrooms') ?? '0')17 const propertyType = searchParams.get('type')1819 let query = supabase20 .from('listings')21 .select('*, listing_images(image_url, position)')22 .eq('status', 'active')23 .gte('price', minPrice)24 .lte('price', maxPrice)25 .gte('bedrooms', bedrooms)2627 if (propertyType && propertyType !== 'all') {28 query = query.eq('property_type', propertyType)29 }3031 if (lat && lng) {32 const { data, error } = await supabase.rpc(33 'search_listings_nearby',34 {35 search_lat: lat,36 search_lng: lng,37 radius_meters: radiusMiles * 1609.34,38 min_price: minPrice,39 max_price: maxPrice,40 min_bedrooms: bedrooms,41 }42 )43 if (error) {44 return NextResponse.json({ error: error.message }, { status: 500 })45 }46 return NextResponse.json({ listings: data })47 }4849 const { data, error } = await query50 .order('created_at', { ascending: false })51 .limit(50)5253 if (error) {54 return NextResponse.json({ error: error.message }, { status: 500 })55 }56 return NextResponse.json({ listings: data })57}Customization ideas
Add interactive map view
Integrate react-map-gl or Google Maps to display listings as map pins with popups showing price and thumbnail. Filter listings by map viewport bounds.
Build a mortgage calculator
Add a mortgage calculator component on each listing page that shows estimated monthly payments based on price, down payment, interest rate, and loan term.
Add virtual tour support
Embed 3D virtual tours from Matterport or 360-degree photos directly on listing pages for remote property viewing.
Build agent profiles
Create agent profile pages showing their listings, contact info, reviews, and transaction history to build trust with buyers.
Common pitfalls
Pitfall: Not enabling PostGIS extension before running spatial queries
How to avoid: Enable PostGIS in Supabase Dashboard: Database > Extensions > search PostGIS > Enable. Then create the spatial index on listing coordinates.
Pitfall: Uploading listing images to a private Supabase Storage bucket
How to avoid: 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.
Pitfall: Fetching all listings on the search page without pagination
How to avoid: 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
AI prompts to try
Copy these prompts to build this project faster.
I'm building a real estate listing app with Next.js App Router, Supabase, and PostGIS. Write a Supabase RPC function called search_listings_nearby that accepts search_lat, search_lng, radius_meters, min_price, max_price, and min_bedrooms. It should use ST_DWithin for the radius filter and ST_Distance for sorting by proximity. Return all listing columns plus distance_meters. Include the CREATE INDEX for the spatial index.
Create a property search filter component with shadcn/ui. Include Input for location, Slider for price range ($0 - $2M), Select for bedrooms (Any/1+/2+/3+/4+), Select for property type (All/House/Condo/Townhouse/Land), and Badge chips for active filters with X to remove. On mobile, wrap in a Sheet. Sync all filter values with URL searchParams using useSearchParams and useRouter.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation