Skip to main content
RapidDev - Software Development Agency

How to Build Classifieds with V0

Build a local classifieds site with V0 using Next.js and Supabase. You'll get item listings with drag-and-drop multi-image uploads, category and location filtering, buyer-seller messaging, content flagging, and full-text search — all in about 1-2 hours without any local setup.

What you'll build

  • Listing gallery with Card grid showing images, price, condition Badge, and location
  • Image upload to Supabase Storage with drag-and-drop and multi-image Carousel preview
  • Full-text search and filtering by category, condition, price range, and location
  • Buyer-seller messaging system with per-listing conversation threads
  • Listing submission form with category Select, condition RadioGroup, and location input
  • Content flagging system with moderator review queue
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a local classifieds site with V0 using Next.js and Supabase. You'll get item listings with drag-and-drop multi-image uploads, category and location filtering, buyer-seller messaging, content flagging, and full-text search — all in about 1-2 hours without any local setup.

What you're building

Local classifieds marketplaces connect buyers and sellers in a community. Think Craigslist or Facebook Marketplace — people post items for sale with photos, others browse by category and location, and interested buyers contact sellers directly.

V0 generates the listing gallery, search interface, messaging system, and moderation tools from prompts. Supabase via the Connect panel provides the database with full-text search, Storage for images, and Auth for user accounts.

The architecture uses Next.js Server Components for the listing pages (fast, SEO-friendly), Supabase full-text search with tsvector for instant search without external services, Supabase Storage for image uploads, a messaging system with per-listing threads, and content flagging for community safety.

Final result

A local classifieds site where users post items with photos, browse by category and location, search with full-text matching, message sellers, and flag inappropriate content — with a moderation queue for administrators.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase StorageStorage
Supabase AuthAuth

Prerequisites

  • A V0 account (Premium plan recommended for multi-feature builds)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Basic understanding of classifieds marketplace concepts (listings, categories, messaging)

Build steps

1

Set up the database schema with listings, categories, and messaging

Create a new V0 project, connect Supabase, and create the tables for listings with full-text search, hierarchical categories, messaging, and content flags.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a classifieds site with Supabase. Create these tables:
3// 1. categories: id (uuid PK), name (text), slug (text unique), icon (text), parent_id (uuid FK self-reference)
4// 2. listings: id (uuid PK), seller_id (uuid FK to auth.users), title (text), description (text), price (numeric), category (text), condition (text CHECK in 'new','like_new','good','fair','poor'), images (text[]), location_city (text), location_state (text), status (text CHECK in 'active','sold','expired','flagged'), expires_at (timestamptz), created_at (timestamptz)
5// 3. messages: id (uuid PK), listing_id (uuid FK), sender_id (uuid FK), receiver_id (uuid FK), content (text), is_read (boolean default false), created_at (timestamptz)
6// 4. flags: id (uuid PK), listing_id (uuid FK), reporter_id (uuid FK), reason (text), created_at (timestamptz)
7// Add a full-text search column on listings: search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || description)) STORED
8// Add a GIN index on search_vector.
9// Add RLS policies.

Pro tip: The generated tsvector column with a GIN index gives you fast full-text search without any external search service. Supabase handles it natively in PostgreSQL.

Expected result: Supabase is connected with all tables created, full-text search configured with a GIN index, and RLS policies applied.

2

Build the listing gallery with search and filters

Create the main browse page with a responsive Card grid, search bar, and filters for category, condition, price range, and location. Use Server Components for SEO-friendly data fetching.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a classifieds browse page at app/listings/page.tsx.
3// Requirements:
4// - Search Input at top with Search icon that uses full-text search via Supabase to_tsquery
5// - Filter sidebar (collapsible Sheet on mobile) with:
6// - Select for category
7// - RadioGroup for condition (new, like new, good, fair, poor)
8// - Price range with two Input fields (min, max)
9// - Input for location (city or state)
10// - Responsive Card grid (2 cols mobile, 3 desktop) with:
11// - Listing image (first from images array) using next/image
12// - Title, price (formatted as currency), condition Badge, location text
13// - Relative timestamp (e.g. "2 hours ago")
14// - Sort Select: newest, price low-high, price high-low
15// - Pagination at bottom
16// - Use Server Components for initial data fetch with searchParams for filters
17// - Skeleton loading states

Expected result: The browse page shows listings in a filterable grid with full-text search, category filters, price range, and condition selection.

3

Create the listing detail page with image carousel and contact form

Build the individual listing page with an image Carousel, seller info, and a contact form that starts a messaging thread between the buyer and seller.

app/listings/[id]/page.tsx
1import { createClient } from '@supabase/supabase-js'
2import { notFound } from 'next/navigation'
3import type { Metadata } from 'next'
4import { ListingDetail } from '@/components/listing-detail'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
12 const { id } = await params
13 const { data: listing } = await supabase.from('listings').select('title, description, price').eq('id', id).single()
14 if (!listing) return {}
15 return {
16 title: `${listing.title} - $${listing.price}`,
17 description: listing.description?.substring(0, 155),
18 }
19}
20
21export default async function ListingPage({ params }: { params: Promise<{ id: string }> }) {
22 const { id } = await params
23 const { data: listing } = await supabase
24 .from('listings')
25 .select('*, profiles:seller_id(full_name, avatar_url)')
26 .eq('id', id)
27 .eq('status', 'active')
28 .single()
29
30 if (!listing) notFound()
31 return <ListingDetail listing={listing} />
32}

Pro tip: Use V0's image upload pattern — generate a Supabase Storage upload component with drag-and-drop that stores images in a public bucket and returns URLs for the images array column.

Expected result: The listing detail page shows an image Carousel, seller info with Avatar, listing details, and a contact seller Dialog that starts a message thread.

4

Build the listing submission form with image uploads

Create the 'post an item' form where sellers can upload photos, enter details, select category and condition, and set a price. Images are uploaded to Supabase Storage.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a listing submission form at app/listings/new/page.tsx ('use client').
3// Requirements:
4// - Input for title
5// - Textarea for description
6// - Input for price with currency formatting
7// - Select for category (fetched from categories table)
8// - RadioGroup for condition: new, like new, good, fair, poor
9// - Image upload area: drag-and-drop multiple images to Supabase Storage 'listings' public bucket
10// - Show image previews, allow reordering, max 8 images, max 5MB each
11// - Accept jpg, png, webp
12// - Input for location city and Select for state
13// - Submit Button that calls a Server Action with all fields including image URLs
14// - Form validation with zod: title required, price > 0, at least one image
15// - Use shadcn/ui Card for form container, Input, Textarea, Select, RadioGroup, Button

Expected result: The submission form collects all listing details with image uploads to Supabase Storage. Validation ensures required fields and at least one image.

5

Add the messaging inbox and content moderation

Build the buyer-seller messaging system and the admin moderation queue for flagged listings.

app/actions/classifieds.ts
1'use server'
2
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function sendMessage(listingId: string, senderId: string, receiverId: string, content: string) {
12 const { error } = await supabase.from('messages').insert({
13 listing_id: listingId,
14 sender_id: senderId,
15 receiver_id: receiverId,
16 content,
17 })
18 if (error) throw new Error(error.message)
19 revalidatePath('/messages')
20}
21
22export async function flagListing(listingId: string, reporterId: string, reason: string) {
23 await supabase.from('flags').insert({ listing_id: listingId, reporter_id: reporterId, reason })
24 const { count } = await supabase.from('flags').select('*', { count: 'exact', head: true }).eq('listing_id', listingId)
25 if ((count ?? 0) >= 3) {
26 await supabase.from('listings').update({ status: 'flagged' }).eq('id', listingId)
27 }
28 revalidatePath(`/listings/${listingId}`)
29}
30
31export async function moderateListing(listingId: string, action: 'approve' | 'remove') {
32 const status = action === 'approve' ? 'active' : 'expired'
33 await supabase.from('listings').update({ status }).eq('id', listingId)
34 if (action === 'approve') {
35 await supabase.from('flags').delete().eq('listing_id', listingId)
36 }
37 revalidatePath('/admin')
38}

Expected result: Buyers can message sellers about listings. Listings with 3+ flags are auto-flagged. Admins can approve or remove flagged listings.

Complete code

app/actions/classifieds.ts
1'use server'
2
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5import { redirect } from 'next/navigation'
6
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function createListing(formData: FormData) {
13 const title = formData.get('title') as string
14 const description = formData.get('description') as string
15 const price = parseFloat(formData.get('price') as string)
16 const category = formData.get('category') as string
17 const condition = formData.get('condition') as string
18 const images = JSON.parse(formData.get('images') as string)
19 const city = formData.get('city') as string
20 const state = formData.get('state') as string
21 const sellerId = formData.get('seller_id') as string
22
23 const expiresAt = new Date()
24 expiresAt.setDate(expiresAt.getDate() + 30)
25
26 const { error } = await supabase.from('listings').insert({
27 seller_id: sellerId,
28 title,
29 description,
30 price,
31 category,
32 condition,
33 images,
34 location_city: city,
35 location_state: state,
36 status: 'active',
37 expires_at: expiresAt.toISOString(),
38 })
39
40 if (error) throw new Error(error.message)
41 revalidatePath('/listings')
42 redirect('/listings')
43}
44
45export async function sendMessage(
46 listingId: string,
47 senderId: string,
48 receiverId: string,
49 content: string
50) {
51 const { error } = await supabase.from('messages').insert({
52 listing_id: listingId,
53 sender_id: senderId,
54 receiver_id: receiverId,
55 content,
56 })
57 if (error) throw new Error(error.message)
58 revalidatePath('/messages')
59}
60
61export async function flagListing(
62 listingId: string,
63 reporterId: string,
64 reason: string
65) {
66 await supabase.from('flags').insert({
67 listing_id: listingId,
68 reporter_id: reporterId,
69 reason,
70 })
71 revalidatePath(`/listings/${listingId}`)
72}

Customization ideas

Add location-based search with PostGIS

Enable the PostGIS extension in Supabase and add a geography column to listings. Create an RPC function for proximity search with ST_DWithin to find listings within a radius.

Add favorites/watchlist

Add a favorites table linking users to listings. Show a heart toggle on each Card and create a Favorites page showing saved items with alerts when prices drop.

Add premium listings

Add a featured field to listings with Stripe payment for premium placement. Featured listings appear at the top of search results with a special Badge.

Add email notifications

Send sellers email notifications when they receive new messages, and send saved search alerts when new listings match a user's search criteria.

Common pitfalls

Pitfall: Using LIKE queries instead of full-text search

How to avoid: Use PostgreSQL full-text search with a generated tsvector column and GIN index. Query with to_tsquery for fast, indexed search across title and description.

Pitfall: Not setting image size limits on upload

How to avoid: Set a 5MB max file size limit on the upload component. Use Supabase Storage transformations for generating thumbnails to serve smaller images in listing Cards.

Pitfall: Allowing sellers to see all messages in the system

How to avoid: Set RLS policies on messages so users can only read messages where they are either the sender_id or receiver_id. Test with different user accounts.

Best practices

  • Use PostgreSQL full-text search with generated tsvector column and GIN index for fast listing search without external services
  • Use Supabase Storage public bucket for listing images with 5MB upload limit and accepted formats (jpg, png, webp)
  • Use Server Components for the browse page to make listings SEO-friendly and server-rendered
  • Add generateMetadata to listing detail pages for dynamic title and description in search results
  • Use Design Mode (Option+D) to adjust listing Card layout, image aspect ratios, and filter panel styling without credits
  • Auto-flag listings with 3+ community reports for moderation review to maintain platform quality
  • Set 30-day expiry on listings with a status check that hides expired listings from search results
  • Use RLS policies on messages so users only see conversations they are part of

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a local classifieds site with Next.js App Router and Supabase. I need listing management with image uploads, full-text search with tsvector, category filtering, buyer-seller messaging, and content moderation with community flagging. Help me design the full-text search setup and the messaging schema.

Build Prompt

Build a Supabase full-text search component for a classifieds site. The search should: use a tsvector generated column on listings (title + description), query with textSearch() or to_tsquery via RPC, combine with category and condition filters in a single query, support sort by relevance (ts_rank), and display results in a responsive Card grid with highlighted matching terms.

Frequently asked questions

How does the full-text search work without external services?

PostgreSQL has built-in full-text search. A generated tsvector column automatically creates a searchable index from the title and description. A GIN index makes queries fast. Supabase exposes this through the textSearch filter or a custom RPC function with to_tsquery.

How do I handle image uploads for listings?

Use Supabase Storage with a public bucket. Create a drag-and-drop upload component that stores images and returns public URLs. Save the URL array in the listings.images column. Set a 5MB max size and accept only jpg, png, and webp.

What V0 plan do I need for a classifieds site?

V0 Premium is recommended because the classifieds site needs multiple pages (browse, detail, post, messages, moderation), image uploads, and full-text search configuration.

How does content moderation work?

Users can flag listings with a reason. When a listing receives 3 or more flags, its status is automatically changed to flagged. Administrators see flagged listings in a moderation queue and can approve or remove them.

How do I deploy the classifieds site?

Click Share then Publish to Production in V0 for instant Vercel deployment. Listing pages are server-rendered for SEO. Supabase Storage serves images via CDN.

Can RapidDev help build a custom classifieds platform?

Yes. RapidDev has built 600+ apps including marketplace platforms with geolocation search, payment escrow, and automated moderation. Book a free consultation to discuss your classifieds 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.