Skip to main content
RapidDev - Software Development Agency

How to Build Map application with V0

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'll build

  • Full-screen interactive map with dynamic markers using react-map-gl (Mapbox wrapper)
  • Radius-based location search using PostGIS ST_DWithin spatial queries
  • Location detail slide-over panel with shadcn/ui Sheet for rich information display
  • Search autocomplete with Command palette for finding locations by name or category
  • User favorites system with save/unsave toggle on location cards
  • Sidebar listing with ScrollArea showing nearby locations sorted by distance
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

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

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
react-map-glMap Rendering
PostGISSpatial Queries

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

1

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.

prompt.txt
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/update
8// 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.

2

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.

app/api/locations/nearby/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export 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')
15
16 let query = supabase.rpc('nearby_locations', {
17 p_lat: lat,
18 p_lng: lng,
19 p_radius_meters: radius,
20 })
21
22 if (category) {
23 query = query.eq('category', category)
24 }
25
26 const { data, error } = await query.limit(50)
27
28 if (error) {
29 return NextResponse.json({ error: error.message }, { status: 500 })
30 }
31
32 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.

3

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.

components/map-view.tsx
1'use client'
2
3import { useState, useCallback } from 'react'
4import Map, { Marker, Popup } from 'react-map-gl'
5import 'mapbox-gl/dist/mapbox-gl.css'
6
7interface Location {
8 id: string
9 name: string
10 category: string
11 latitude: number
12 longitude: number
13 address: string
14}
15
16interface MapViewProps {
17 locations: Location[]
18 onSelect: (location: Location) => void
19}
20
21export function MapView({ locations, onSelect }: MapViewProps) {
22 const [popup, setPopup] = useState<Location | null>(null)
23
24 return (
25 <Map
26 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 <Marker
33 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 <Popup
45 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.

4

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.

prompt.txt
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 Supabase
5// - 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 name
8// - Below search: ScrollArea with location Cards showing name, category Badge, address, distance
9// - Clicking a sidebar Card highlights the marker on the map
10// - 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 list
12// - Add Skeleton loading states for the map and sidebar while data loads
13// - Mobile layout: map full-screen with a bottom Sheet for the sidebar

Expected result: The map page shows an interactive map with a sidebar listing, search autocomplete, category filters, and a location detail Sheet.

5

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.

supabase/migrations/nearby_locations.sql
1-- Run this in Supabase SQL Editor
2CREATE OR REPLACE FUNCTION nearby_locations(
3 p_lat double precision,
4 p_lng double precision,
5 p_radius_meters int DEFAULT 5000
6)
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 precision
18) AS $$
19BEGIN
20 RETURN QUERY
21 SELECT
22 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)::geography
28 ) AS distance_meters
29 FROM locations l
30 WHERE ST_DWithin(
31 l.geog,
32 ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
33 p_radius_meters
34 )
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

app/api/locations/nearby/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export 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 50000
16 )
17 const category = searchParams.get('category')
18
19 const { data, error } = await supabase.rpc('nearby_locations', {
20 p_lat: lat,
21 p_lng: lng,
22 p_radius_meters: radius,
23 })
24
25 if (error) {
26 return NextResponse.json({ error: error.message }, { status: 500 })
27 }
28
29 const filtered = category
30 ? data?.filter((l: { category: string }) => l.category === category)
31 : data
32
33 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.

ChatGPT Prompt

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.

Build Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.