Build a star rating and review system with V0 using Next.js and Supabase that adds user reviews with 1-5 star ratings, helpful votes, photo uploads, and basic moderation to any product or service app. Features aggregate stats via database triggers — all in about 30-60 minutes.
What you're building
Reviews and ratings build trust. Whether you are selling products, listing services, or running a marketplace, user-generated reviews with star ratings directly impact purchase decisions. A good review system needs to be easy to submit, hard to spam, and fast to display aggregate scores.
V0 generates the review form, star rating component, review list, and moderation queue from prompts. The key architectural decision is using Supabase database triggers to maintain aggregate stats (average rating, total reviews, rating distribution) so you never need expensive COUNT queries on the reviews table.
The architecture uses Next.js App Router with Server Components for the reviews list (SEO-friendly), a client component for the interactive star rating, Server Actions for all mutations, Supabase triggers for maintaining aggregate stats, and Supabase Storage for review photos.
Final result
A review and rating system with 5-star ratings, text reviews with photos, helpful vote buttons, verified purchase badges, admin moderation, and always-current aggregate statistics.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A product, service, or item you want users to review
Build steps
Set up the project and reviews schema with triggers
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the schema for reviews, images, helpful votes, and aggregate stats. Add a database trigger to auto-update stats on every review insert or update.
1// Paste this prompt into V0's AI chat:2// Build a reviews and ratings system. Create a Supabase schema with:3// 1. reviews: id (uuid PK), author_id (uuid FK), item_id (uuid FK), item_type (text check product/service/business), rating (integer check 1-5), title (text), body (text), is_verified_purchase (boolean default false), status (text check pending/approved/rejected/flagged), created_at (timestamptz), updated_at (timestamptz)4// 2. review_images: id (uuid PK), review_id (uuid FK), image_url (text), position (integer)5// 3. helpful_votes: id (uuid PK), review_id (uuid FK), user_id (uuid FK), created_at (timestamptz) with unique constraint on (review_id, user_id)6// 4. review_stats: item_id (uuid PK), average_rating (numeric), total_reviews (integer), rating_distribution (jsonb), updated_at (timestamptz)7// Create a database trigger on reviews INSERT/UPDATE that recalculates review_stats for the affected item_id.8// RLS: anyone can SELECT approved reviews, authenticated users can INSERT, only author/admin can UPDATE.9// Generate SQL migration and TypeScript types.Pro tip: The database trigger keeps review_stats always current without expensive COUNT queries — this is critical for performance as your review count grows.
Expected result: Supabase is connected with reviews, images, votes, and stats tables. A database trigger automatically recalculates aggregate stats whenever a review is added or updated.
Build the star rating component and review form
Create a reusable star rating input component and the review submission form. The star component uses Lucide Star icons with hover and click states. The form includes rating, title, body, and photo upload.
1'use client'23import { useState } from 'react'4import { Star } from 'lucide-react'5import { cn } from '@/lib/utils'67export function StarRating({8 value,9 onChange,10 readonly = false,11}: {12 value: number13 onChange?: (rating: number) => void14 readonly?: boolean15}) {16 const [hover, setHover] = useState(0)1718 return (19 <div className="flex gap-1">20 {[1, 2, 3, 4, 5].map((star) => (21 <Star22 key={star}23 className={cn(24 'w-6 h-6 transition-colors',25 (hover || value) >= star26 ? 'fill-yellow-400 text-yellow-400'27 : 'text-muted-foreground',28 !readonly && 'cursor-pointer'29 )}30 onMouseEnter={() => !readonly && setHover(star)}31 onMouseLeave={() => !readonly && setHover(0)}32 onClick={() => onChange?.(star)}33 />34 ))}35 </div>36 )37}Expected result: A reusable star rating component with hover and click states. Stars fill with yellow on hover and lock on click. Can be used as input (interactive) or display (readonly).
Create the reviews list with helpful votes
Build the reviews display for an item page. Reviews are sorted by most helpful or most recent, with aggregate stats at the top showing average rating and distribution breakdown.
1// Paste this prompt into V0's AI chat:2// Build a reviews section for app/items/[id]/reviews/page.tsx.3// Requirements:4// - Top section: aggregate stats from review_stats table5// - Large average rating number with StarRating (readonly), total review count6// - Rating distribution bars: 5 rows (5 stars to 1 star), each with a Progress bar and count7// - Sort controls: DropdownMenu with options: Most Helpful, Newest, Highest Rated, Lowest Rated8// - Review list: shadcn/ui Card for each review showing:9// - Avatar with author name, StarRating (readonly), date10// - Badge for "Verified Purchase" if is_verified_purchase11// - Review title (bold), body text12// - Review photos as small image thumbnails (if any)13// - "Helpful" Button with count — clicking calls Server Action markHelpful() with optimistic update14// - "Write a Review" Button at top opens the review form Dialog15// - Use Server Components for data fetching, 'use client' for star rating and helpful vote16// - Server Actions: submitReview(), markHelpful()Pro tip: Use V0's Design Mode (Option+D) to visually adjust the star rating colors, review Card spacing, and image gallery layout without spending credits.
Expected result: A reviews section showing aggregate stats with distribution bars, sortable review Cards with star ratings, helpful vote buttons, and verified purchase Badges.
Add the admin moderation queue
Build an admin page for moderating reviews. New reviews start with status 'pending' and need approval before they appear publicly. Admins can approve, reject, or flag reviews.
1// Paste this prompt into V0's AI chat:2// Build a review moderation page at app/reviews/page.tsx.3// Requirements:4// - Protected by admin auth5// - Tabs: Pending, Approved, Rejected, Flagged6// - Table of reviews showing: item name, author, rating (stars), title, body preview (50 chars), status Badge, date7// - Each row has action Buttons:8// - Approve (green): sets status to 'approved', triggers stats recalculation9// - Reject (red): opens AlertDialog for rejection reason, sets status to 'rejected'10// - Flag (yellow): sets status to 'flagged' for further review11// - Bulk action: Checkbox selection with bulk approve/reject Buttons12// - Server Actions: moderateReview() that updates status and logs the action13// - Server Components for data fetching14// - Use shadcn/ui Table, Badge, Button, AlertDialog, Checkbox, TabsExpected result: An admin moderation queue with tabs for review statuses, a Table of reviews with approve/reject/flag actions, and bulk moderation capabilities.
Complete code
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'56export async function submitReview(formData: FormData) {7 const supabase = await createClient()8 const { data: { user } } = await supabase.auth.getUser()9 if (!user) throw new Error('Must be logged in')1011 const { data: review } = await supabase12 .from('reviews')13 .insert({14 author_id: user.id,15 item_id: formData.get('item_id') as string,16 item_type: formData.get('item_type') as string,17 rating: parseInt(formData.get('rating') as string),18 title: formData.get('title') as string,19 body: formData.get('body') as string,20 status: 'pending',21 })22 .select()23 .single()2425 if (!review) throw new Error('Failed to submit review')2627 const itemId = formData.get('item_id') as string28 revalidatePath(`/items/${itemId}/reviews`)29 return review30}3132export async function markHelpful(reviewId: string) {33 const supabase = await createClient()34 const { data: { user } } = await supabase.auth.getUser()35 if (!user) throw new Error('Must be logged in')3637 const { error } = await supabase38 .from('helpful_votes')39 .insert({ review_id: reviewId, user_id: user.id })4041 if (error && error.code !== '23505') {42 throw new Error(error.message)43 }44}4546export async function moderateReview(47 reviewId: string,48 status: 'approved' | 'rejected' | 'flagged'49) {50 const supabase = await createClient()5152 await supabase53 .from('reviews')54 .update({ status, updated_at: new Date().toISOString() })55 .eq('id', reviewId)5657 revalidatePath('/reviews')58}Customization ideas
Add review responses from owners
Let business or product owners reply to reviews publicly, showing a threaded response below the original review.
Build spam detection
Use OpenAI to automatically flag suspicious reviews based on content patterns — fake enthusiasm, copied text, or review bombing within short timeframes.
Add review analytics
Build a dashboard showing review trends over time, sentiment analysis, and keyword extraction using Recharts visualizations.
Implement review incentives
Send automated email reminders asking verified purchasers to leave reviews, with optional reward points or discount codes for completion.
Common pitfalls
Pitfall: Computing average ratings with a COUNT query on every page load
How to avoid: Use a Supabase database trigger on reviews INSERT/UPDATE that recalculates review_stats for the affected item_id. The display page reads from the pre-computed stats table.
Pitfall: Allowing the star rating component to render server-side
How to avoid: Mark the star rating component with 'use client'. When displaying reviews, use a readonly version of the same component.
Pitfall: Not handling the 23505 duplicate key error for helpful votes
How to avoid: Catch the 23505 error code in the markHelpful Server Action and silently ignore it — the vote is already recorded.
Best practices
- Use Supabase database triggers to maintain aggregate review_stats instead of computing averages on every page load
- Use V0's Design Mode (Option+D) to visually adjust star colors, review Card spacing, and image thumbnails for free
- Store review images in a Supabase Storage public bucket for permanent, SEO-friendly URLs
- Handle the Supabase 23505 duplicate key error gracefully for helpful vote deduplication
- Use Server Components for the reviews list to optimize SEO — reviews are valuable search content
- Start reviews with status 'pending' and require admin approval to prevent spam from appearing publicly
AI prompts to try
Copy these prompts to build this project faster.
I'm building a reviews system with Next.js App Router and Supabase. Write a PostgreSQL trigger function that fires on INSERT or UPDATE on the reviews table. It should recalculate the review_stats row for the affected item_id: compute average_rating, total_reviews (only counting approved reviews), and rating_distribution as a JSONB object mapping each star level (1-5) to its count. Include the CREATE TRIGGER statement.
Create a review aggregate stats component. Accept review_stats data (average_rating, total_reviews, rating_distribution). Display the average as a large number with readonly StarRating, total count, and 5 rating distribution rows. Each row shows '5 stars' label, a shadcn/ui Progress bar proportional to that rating's percentage, and the count number. Use Server Components for data fetching.
Frequently asked questions
Can I build this on V0's free tier?
Yes. The free tier provides enough credits to generate the star rating component, review form, reviews list, and moderation page. Supabase free tier handles the database and image storage.
How do aggregate stats stay up to date?
A Supabase database trigger fires on every review INSERT or UPDATE. It recalculates the average_rating, total_reviews, and rating_distribution for the affected item and updates the review_stats table. The display page reads from this pre-computed table.
How do I prevent fake reviews?
The build includes three layers: authentication (only logged-in users can review), moderation queue (admin approves before public display), and verified purchase badges. You can add AI-based spam detection as a customization.
Can I use this for any type of item (products, services, businesses)?
Yes. The item_type field (product/service/business) makes the system generic. Reviews are linked to any item via item_id, so you can attach reviews to products, services, courses, or any entity in your app.
How do I deploy the reviews system?
Click Share then Publish to Production in V0. Review pages are server-rendered for SEO. Supabase credentials are auto-configured from the Connect panel.
Can RapidDev help build a custom reviews platform?
Yes. RapidDev has built 600+ apps including review platforms with AI moderation, sentiment analysis, and review incentive 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