Build an interactive map application with V0 using Next.js, Supabase with PostGIS, react-map-gl, and shadcn/ui. Features dynamic markers, radius-based search, location detail panels, and user favorites — with proper SSR handling for Mapbox GL. Takes about 1-2 hours to complete.
What you're building
Directory apps, delivery platforms, and real estate listings all need interactive maps. Building one from scratch with proper spatial queries and SSR compatibility is tricky — Mapbox GL requires the browser window object that does not exist during server-side rendering.
V0 generates the map layout, sidebar, and search components from prompts. Supabase with PostGIS handles the spatial queries (find locations within X kilometers), and react-map-gl provides the Mapbox wrapper that integrates with React.
The architecture uses a Server Component parent that preloads location data, a dynamically imported client component for the map (bypassing SSR), PostGIS for radius queries, and shadcn/ui Sheet for the location detail panel.
Final result
An interactive map application with location markers, radius search, detail panels, search autocomplete, and a favorites system.
Tech stack
Prerequisites
- A V0 account (Premium or higher recommended)
- A Supabase project with PostGIS extension enabled (free tier works)
- A Mapbox account for the map token (free tier: 50K map loads/month)
- Location data with latitude and longitude coordinates
Build steps
Set up Supabase with PostGIS and location schema
Connect Supabase via the Connect panel, enable the PostGIS extension, and create the locations schema with spatial columns and indexes for fast radius queries.
1// Paste this prompt into V0's AI chat:2// Build a map application. First, I need to set up Supabase with spatial queries.3// Create this SQL migration:4// 1. Enable PostGIS: CREATE EXTENSION IF NOT EXISTS postgis;5// 2. Create locations table: id (uuid PK), name (text), description (text), category (text), latitude (numeric), longitude (numeric), address (text), metadata (jsonb), image_url (text), created_at (timestamptz)6// 3. Add a geography column: ALTER TABLE locations ADD COLUMN geog geography(Point, 4326);7// 4. Create trigger to auto-update geog from lat/lng on insert/update8// 5. Create spatial index: CREATE INDEX idx_locations_geog ON locations USING GIST (geog);9// 6. Create user_favorites table: user_id (uuid FK to auth.users), location_id (uuid FK to locations), PRIMARY KEY (user_id, location_id)10// Add RLS policies: anyone can read locations, authenticated users manage favorites.Pro tip: Enable PostGIS in Supabase Dashboard under Database > Extensions before running the migration. Without it, the geography column and spatial functions will fail.
Expected result: PostGIS is enabled, the locations table has a spatial index on the geography column, and the favorites table is ready.
Build the radius search API with PostGIS
Create the API route that accepts latitude, longitude, and radius parameters and returns nearby locations using PostGIS ST_DWithin for efficient spatial queries.
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'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 radius = parseInt(searchParams.get('radius') || '5000')14 const category = searchParams.get('category')1516 let query = supabase.rpc('nearby_locations', {17 p_lat: lat,18 p_lng: lng,19 p_radius_meters: radius,20 })2122 if (category) {23 query = query.eq('category', category)24 }2526 const { data, error } = await query.limit(50)2728 if (error) {29 return NextResponse.json({ error: error.message }, { status: 500 })30 }3132 return NextResponse.json({ data })33}Expected result: The API returns locations within the specified radius, sorted by distance. Spatial index makes queries fast even with thousands of locations.
Create the map component with SSR-safe dynamic import
Build the interactive map using react-map-gl wrapped in a 'use client' component. Import it dynamically with next/dynamic and ssr: false to prevent server-side rendering errors from Mapbox GL's window dependency.
1'use client'23import { useState, useCallback } from 'react'4import Map, { Marker, Popup } from 'react-map-gl'5import 'mapbox-gl/dist/mapbox-gl.css'67interface Location {8 id: string9 name: string10 category: string11 latitude: number12 longitude: number13 address: string14}1516interface MapViewProps {17 locations: Location[]18 onSelect: (location: Location) => void19}2021export function MapView({ locations, onSelect }: MapViewProps) {22 const [popup, setPopup] = useState<Location | null>(null)2324 return (25 <Map26 mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}27 initialViewState={{ longitude: -73.98, latitude: 40.75, zoom: 12 }}28 style={{ width: '100%', height: '100%' }}29 mapStyle="mapbox://styles/mapbox/light-v11"30 >31 {locations.map((loc) => (32 <Marker33 key={loc.id}34 longitude={loc.longitude}35 latitude={loc.latitude}36 onClick={(e) => {37 e.originalEvent.stopPropagation()38 setPopup(loc)39 onSelect(loc)40 }}41 />42 ))}43 {popup && (44 <Popup45 longitude={popup.longitude}46 latitude={popup.latitude}47 onClose={() => setPopup(null)}48 >49 <div className="p-2">50 <h3 className="font-semibold">{popup.name}</h3>51 <p className="text-sm text-muted-foreground">{popup.address}</p>52 </div>53 </Popup>54 )}55 </Map>56 )57}Pro tip: Store NEXT_PUBLIC_MAPBOX_TOKEN in V0's Vars tab with the NEXT_PUBLIC_ prefix — Mapbox tokens are publishable keys designed to be used in the browser.
Expected result: The map renders with dynamic markers for each location. Clicking a marker shows a popup and triggers the detail panel.
Build the main map page with sidebar and dynamic import
Create the full map page that combines the server-loaded location data, dynamically imported map component, sidebar listing, and search autocomplete. The parent is a Server Component that passes data to client children.
1// Paste this prompt into V0's AI chat:2// Create a map application page at app/map/page.tsx.3// Requirements:4// - Server Component that fetches initial locations from Supabase5// - Dynamically import the MapView component with next/dynamic and { ssr: false }6// - Layout: full-screen map on the right (70% width), sidebar on the left (30% width)7// - Sidebar has: Command palette search at top for finding locations by name8// - Below search: ScrollArea with location Cards showing name, category Badge, address, distance9// - Clicking a sidebar Card highlights the marker on the map10// - Clicking a marker opens a shadcn/ui Sheet slide-over with full location details: image, description, metadata, category Badge, address, and a favorite toggle Button (heart icon)11// - Add category filter as a row of Badge-style toggle buttons above the sidebar list12// - Add Skeleton loading states for the map and sidebar while data loads13// - Mobile layout: map full-screen with a bottom Sheet for the sidebarExpected result: The map page shows an interactive map with a sidebar listing, search autocomplete, category filters, and a location detail Sheet.
Add the PostGIS radius search function and deploy
Create the Supabase RPC function for spatial queries, add the favorites Server Action, and deploy the application.
1-- Run this in Supabase SQL Editor2CREATE OR REPLACE FUNCTION nearby_locations(3 p_lat double precision,4 p_lng double precision,5 p_radius_meters int DEFAULT 50006)7RETURNS TABLE (8 id uuid,9 name text,10 description text,11 category text,12 latitude numeric,13 longitude numeric,14 address text,15 metadata jsonb,16 image_url text,17 distance_meters double precision18) AS $$19BEGIN20 RETURN QUERY21 SELECT22 l.id, l.name, l.description, l.category,23 l.latitude, l.longitude, l.address,24 l.metadata, l.image_url,25 ST_Distance(26 l.geog,27 ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography28 ) AS distance_meters29 FROM locations l30 WHERE ST_DWithin(31 l.geog,32 ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,33 p_radius_meters34 )35 ORDER BY distance_meters;36END;37$$ LANGUAGE plpgsql SECURITY DEFINER;Expected result: The RPC function returns locations within the specified radius sorted by distance. Deploy via Share > Publish in V0.
Complete code
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'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 radius = Math.min(14 parseInt(searchParams.get('radius') || '5000'),15 5000016 )17 const category = searchParams.get('category')1819 const { data, error } = await supabase.rpc('nearby_locations', {20 p_lat: lat,21 p_lng: lng,22 p_radius_meters: radius,23 })2425 if (error) {26 return NextResponse.json({ error: error.message }, { status: 500 })27 }2829 const filtered = category30 ? data?.filter((l: { category: string }) => l.category === category)31 : data3233 return NextResponse.json({34 data: filtered,35 meta: { lat, lng, radius, count: filtered?.length || 0 },36 })37}Customization ideas
Heatmap layer for density visualization
Add a Mapbox heatmap layer that shows location density, useful for identifying clusters in directory or real estate apps.
Directions and routing
Integrate the Mapbox Directions API to show driving or walking routes from the user's current location to a selected destination.
User-submitted locations
Add a form for users to submit new locations with geocoding (convert address to lat/lng) using the Mapbox Geocoding API.
Clustering for large datasets
Enable Mapbox's built-in marker clustering to group nearby markers when zoomed out, improving performance with thousands of locations.
Common pitfalls
Pitfall: Importing react-map-gl in a Server Component without dynamic import
How to avoid: Use next/dynamic with { ssr: false } to dynamically import the map component only on the client side.
Pitfall: Not creating a spatial index on the geography column
How to avoid: Create a GIST index: CREATE INDEX idx_locations_geog ON locations USING GIST (geog). This makes radius queries nearly instant.
Pitfall: Using NEXT_PUBLIC_ prefix for SUPABASE_SERVICE_ROLE_KEY
How to avoid: Use the service role key only in API routes without any prefix. The Mapbox token is safe with NEXT_PUBLIC_ because it is a publishable key.
Best practices
- Use next/dynamic with { ssr: false } for the map component to avoid window-related SSR errors
- Enable PostGIS and create a GIST spatial index before deploying — radius queries need it for performance
- Store NEXT_PUBLIC_MAPBOX_TOKEN in V0's Vars tab — Mapbox tokens are publishable keys safe for client-side use
- Preload location data in the Server Component parent and pass it as props to the client map component
- Cap the radius parameter in the API route (e.g., max 50km) to prevent queries that scan the entire table
- Use V0's Design Mode (Option+D) to adjust the sidebar width, card layout, and map controls without spending credits
- Add Skeleton loading states for both the map and sidebar to prevent layout shifts during data loading
AI prompts to try
Copy these prompts to build this project faster.
I'm building a map application with Next.js App Router and Supabase with PostGIS. I need a PostgreSQL function that finds all locations within a given radius of a point. The function should accept latitude, longitude, and radius in meters, use ST_DWithin for the spatial query, and return results sorted by distance. My locations table has a geography(Point, 4326) column called geog. Please write the SQL function and the Next.js API route that calls it.
Create a react-map-gl Map component with these features: dynamic Marker components rendered from a locations array prop, a Popup that appears on marker click showing location name and address, a NavigationControl for zoom buttons, a GeolocateControl for user location, and a callback prop onBoundsChange that fires when the map viewport changes (for loading new locations in the visible area). Make sure to import mapbox-gl/dist/mapbox-gl.css for proper styling.
Frequently asked questions
Why do I need PostGIS for the map application?
PostGIS adds spatial query capabilities to PostgreSQL. Without it, finding locations within a radius requires calculating distances for every row in JavaScript — extremely slow with thousands of locations. PostGIS uses spatial indexes to do this in milliseconds.
Is the Mapbox token safe to expose in the browser?
Yes. Mapbox tokens are publishable keys designed to be used in frontend code. You can restrict the token to specific URLs in the Mapbox Dashboard for additional security. Use NEXT_PUBLIC_MAPBOX_TOKEN in V0's Vars tab.
How do I handle the SSR error with Mapbox GL?
Mapbox GL requires the window object which does not exist on the server. Use next/dynamic to import the map component with { ssr: false }, which ensures it only loads in the browser.
Do I need a paid V0 plan?
Premium ($20/month) is recommended. The map application has complex components (map, sidebar, search, detail panel) that require multiple prompts to build.
Can I use Google Maps instead of Mapbox?
Yes, but react-map-gl wraps Mapbox GL which offers a generous free tier (50K loads/month). For Google Maps, use @vis.gl/react-google-maps instead. The Supabase PostGIS backend works the same regardless of which map provider you choose.
How do I deploy the map application?
Click Share in V0, then Publish to Production. Make sure PostGIS is enabled in your Supabase project, the spatial index is created, and NEXT_PUBLIC_MAPBOX_TOKEN is set in the Vars tab.
Can RapidDev help build a custom map application?
Yes. RapidDev has built over 600 apps including map-based platforms for real estate, delivery, and directory services with PostGIS, clustering, and routing. Book a free consultation to discuss your map requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation