Build a feedback collection tool with V0 using Next.js, Supabase for feedback storage, and an embeddable widget for any website. You'll create a public feedback board with upvoting, team response threads, status tracking, and a JavaScript widget snippet — all in about 1-2 hours without touching a terminal.
What you're building
Product teams need a structured way to collect user feedback, prioritize feature requests, and communicate progress. A dedicated feedback tool replaces scattered emails and support tickets with a centralized board where users can submit ideas, upvote favorites, and see what's planned.
V0 generates the feedback board UI, admin controls, and embeddable widget from prompts. The widget is a lightweight JavaScript snippet that injects an iframe pointing to your deployed domain, using postMessage for cross-origin communication. Supabase stores all feedback data with RLS policies.
The architecture uses Next.js App Router with Server Components for the public board, client components for upvoting and submission, an API route for widget submissions with Origin header validation, and Server Actions for admin status management.
Final result
A complete feedback collection platform with a public voting board, embeddable widget, admin dashboard with team responses, and category-based prioritization.
Tech stack
Prerequisites
- A V0 account (Premium plan for widget and board iterations)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A product or website where you want to collect feedback
- A domain where the feedback widget will be embedded
Build steps
Set up the project and feedback database schema
Open V0 and create a new project. Connect Supabase via the Connect panel. Create the schema for projects, feedback items, upvotes, and comments.
1// Paste this prompt into V0's AI chat:2// Build a feedback collection tool. Create a Supabase schema with:3// 1. projects: id (uuid PK), name (text), owner_id (uuid FK to auth.users), widget_key (text unique default gen_random_uuid()), domain (text), created_at (timestamptz)4// 2. feedback_items: id (uuid PK), project_id (uuid FK to projects), title (text), description (text), category (text default 'feature' check in 'feature','bug','improvement','question'), status (text default 'new' check in 'new','under_review','planned','in_progress','completed','declined'), priority (int default 0), submitter_email (text), submitter_name (text), user_id (uuid FK to auth.users nullable), upvote_count (int default 0), created_at (timestamptz)5// 3. upvotes: id (uuid PK), feedback_id (uuid FK to feedback_items), user_id (uuid FK to auth.users), created_at (timestamptz), unique(feedback_id, user_id)6// 4. comments: id (uuid PK), feedback_id (uuid FK to feedback_items), user_id (uuid FK to auth.users), body (text), is_team_reply (boolean default false), created_at (timestamptz)7// Add RLS: anyone can read feedback for published projects, authenticated users can upvote and submit.Pro tip: The widget_key is auto-generated as a UUID. Use it to identify which project the widget belongs to when receiving submissions from external sites.
Expected result: Supabase is connected with all four tables. The widget_key column provides a unique identifier for each project's embeddable widget.
Build the public feedback board with upvoting
Create the public-facing feedback board where users browse feature requests, filter by category and status, and upvote their favorites. Upvoting uses optimistic UI for instant feedback.
1// Paste this prompt into V0's AI chat:2// Build a public feedback board at app/board/[project_id]/page.tsx.3// Requirements:4// - Server Component that fetches feedback_items for this project, ordered by upvote_count DESC5// - Each feedback item as a Card with: upvote Button (arrow up + count) on the left, title, description preview (2 lines), category Badge, status Badge with color coding6// - Filter bar with Tabs for status views (All, Planned, In Progress, Completed)7// - Select for category filter (feature/bug/improvement/question)8// - Sort options: Most Upvoted, Newest, Recently Updated9// - Upvote Button is a 'use client' component that calls a Server Action to toggle the upvote with optimistic UI10// - The Server Action upserts into upvotes (insert if not exists, delete if exists) and updates upvote_count via RPC11// - "Submit Feedback" Button at top that opens a Dialog with title Input, description Textarea, category Select, and submitter email Input12// - Feedback submission goes through a Server Action with Zod validation (title 5-100 chars, description 10-500 chars)13// - Use Separator between the filter bar and the feedback listExpected result: The board shows feedback items sorted by upvotes with category and status filters. Users can upvote (toggling) and submit new feedback.
Create the widget API endpoint with origin validation
Build the API route that receives feedback submissions from the embeddable widget. It validates the widget key and checks the Origin header against the project's registered domain to prevent abuse.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { z } from 'zod'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910const widgetSchema = z.object({11 widget_key: z.string().uuid(),12 title: z.string().min(5).max(100),13 description: z.string().min(10).max(500),14 category: z.enum(['feature', 'bug', 'improvement', 'question']),15 submitter_email: z.string().email().optional(),16 submitter_name: z.string().max(100).optional(),17})1819export async function POST(req: NextRequest) {20 const origin = req.headers.get('origin')21 const body = await req.json()2223 const parsed = widgetSchema.safeParse(body)24 if (!parsed.success) {25 return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })26 }2728 const { data: project } = await supabase29 .from('projects')30 .select('id, domain')31 .eq('widget_key', parsed.data.widget_key)32 .single()3334 if (!project) {35 return NextResponse.json({ error: 'Invalid widget key' }, { status: 403 })36 }3738 if (project.domain && origin && !origin.includes(project.domain)) {39 return NextResponse.json({ error: 'Origin not allowed' }, { status: 403 })40 }4142 await supabase.from('feedback_items').insert({43 project_id: project.id,44 title: parsed.data.title,45 description: parsed.data.description,46 category: parsed.data.category,47 submitter_email: parsed.data.submitter_email,48 submitter_name: parsed.data.submitter_name,49 })5051 return NextResponse.json({ success: true }, {52 headers: {53 'Access-Control-Allow-Origin': origin ?? '*',54 'Access-Control-Allow-Methods': 'POST, OPTIONS',55 'Access-Control-Allow-Headers': 'Content-Type',56 },57 })58}5960export async function OPTIONS(req: NextRequest) {61 return NextResponse.json({}, {62 headers: {63 'Access-Control-Allow-Origin': req.headers.get('origin') ?? '*',64 'Access-Control-Allow-Methods': 'POST, OPTIONS',65 'Access-Control-Allow-Headers': 'Content-Type',66 },67 })68}Pro tip: Use Design Mode (Option+D) to style the public board and widget to match your product's brand colors — free visual adjustments without spending credits.
Expected result: The widget API validates the key and origin, inserts feedback, and returns CORS headers for cross-origin widget requests.
Build the admin dashboard with team replies
Create the admin dashboard where product teams view feedback stats, change status, set priority, and reply to feedback items with team-flagged comments.
1// Paste this prompt into V0's AI chat:2// Build an admin dashboard at app/page.tsx.3// Requirements:4// - Project selector at top if user has multiple projects5// - Stats Cards: total feedback, open items, planned features, completed6// - Feedback Table with columns: title, category Badge, status Badge, upvotes count, submitter, date7// - Clicking a row opens a Sheet (slide from right) with full feedback detail:8// - Title, description, submitter info9// - Status change via Select dropdown (new, under_review, planned, in_progress, completed, declined)10// - Priority slider or Select (low, medium, high, critical)11// - Comment thread showing all comments with Avatar, name, body, and "Team" Badge for is_team_reply=true12// - Textarea + Button for adding a team reply (is_team_reply set to true)13// - Quick status change via Popover on the status Badge in the table14// - Filter by category using Tabs, sort by upvotes or date15// - Use Card for stats and Separator between sectionsExpected result: The admin dashboard shows all feedback with stats. Clicking an item opens a detail Sheet with status controls and a team reply thread.
Create the embeddable widget snippet
Build a simple JavaScript snippet that customers add to their website. It injects an iframe pointing to your feedback form, using postMessage for cross-origin communication.
1// Paste this prompt into V0's AI chat:2// Build the embeddable feedback widget system.3// 1. Create app/widget/[widget_key]/page.tsx — a minimal 'use client' page with:4// - A floating Button (bottom-right, fixed) labeled "Feedback" with a message icon5// - Clicking it expands to show a compact form: title Input, description Textarea, category Select, optional email Input6// - Submit POSTs to /api/widget with the widget_key7// - Show success message after submission8// - Styled with Tailwind in a compact card layout suitable for iframe embedding9// - Use minimal styles so it looks good as a small widget10// 2. Create app/api/embed/route.ts that returns a JavaScript snippet:11// - The snippet creates an iframe pointing to your-domain.com/widget/{widget_key}12// - Positions it fixed bottom-right with z-index 999913// - Starts collapsed (just a button), expands on click14// - Customers add this script tag to their HTML: <script src="your-domain.com/api/embed?key=WIDGET_KEY"></script>15// Set NEXT_PUBLIC_APP_URL in Vars tab to the production domain for the iframe src.Expected result: Customers can add a single script tag to their website. It shows a feedback button that expands into a submission form embedded in an iframe.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { z } from 'zod'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910const schema = z.object({11 widget_key: z.string().uuid(),12 title: z.string().min(5).max(100),13 description: z.string().min(10).max(500),14 category: z.enum(['feature', 'bug', 'improvement', 'question']),15 submitter_email: z.string().email().optional(),16 submitter_name: z.string().max(100).optional(),17})1819export async function POST(req: NextRequest) {20 const origin = req.headers.get('origin')21 const body = await req.json()22 const parsed = schema.safeParse(body)2324 if (!parsed.success) {25 return NextResponse.json(26 { error: parsed.error.flatten() },27 { status: 400 }28 )29 }3031 const { data: project } = await supabase32 .from('projects')33 .select('id, domain')34 .eq('widget_key', parsed.data.widget_key)35 .single()3637 if (!project) {38 return NextResponse.json(39 { error: 'Invalid widget key' },40 { status: 403 }41 )42 }4344 if (project.domain && origin && !origin.includes(project.domain)) {45 return NextResponse.json(46 { error: 'Origin not allowed' },47 { status: 403 }48 )49 }5051 await supabase.from('feedback_items').insert({52 project_id: project.id,53 title: parsed.data.title,54 description: parsed.data.description,55 category: parsed.data.category,56 submitter_email: parsed.data.submitter_email,57 submitter_name: parsed.data.submitter_name,58 })5960 const corsHeaders = {61 'Access-Control-Allow-Origin': origin ?? '*',62 'Access-Control-Allow-Methods': 'POST, OPTIONS',63 'Access-Control-Allow-Headers': 'Content-Type',64 }6566 return NextResponse.json({ success: true }, { headers: corsHeaders })67}6869export async function OPTIONS(req: NextRequest) {70 return NextResponse.json({}, {71 headers: {72 'Access-Control-Allow-Origin': req.headers.get('origin') ?? '*',73 'Access-Control-Allow-Methods': 'POST, OPTIONS',74 'Access-Control-Allow-Headers': 'Content-Type',75 },76 })77}Customization ideas
Add voting limits per user
Restrict each user to a set number of upvotes per month to prevent vote manipulation and encourage thoughtful prioritization.
Add public roadmap view
Create a Kanban-style public roadmap page that shows feedback items grouped by status (planned, in progress, completed) so users can see what's coming.
Add email notifications for status changes
Send email via Resend to feedback submitters when their item's status changes, keeping them engaged and informed about progress.
Add sentiment analysis with AI
Route feedback through an OpenAI API call to automatically categorize sentiment (positive/negative/neutral) and extract key themes.
Common pitfalls
Pitfall: Not validating the Origin header in the widget API endpoint
How to avoid: Store the allowed domain in the projects table and check the request Origin header against it. Return 403 if the origin doesn't match.
Pitfall: Allowing duplicate upvotes by not enforcing the unique constraint
How to avoid: Add the unique constraint in the database and use upsert with onConflict in the Server Action. Toggle the upvote by checking if it exists first.
Pitfall: Incrementing upvote_count without using atomic operations
How to avoid: Use a Supabase RPC function that atomically increments or decrements upvote_count in a single SQL statement.
Best practices
- Validate the Origin header in the widget API to prevent cross-origin abuse from unauthorized domains.
- Use a unique(feedback_id, user_id) constraint on upvotes to prevent duplicate votes at the database level.
- Implement optimistic UI for upvote toggling — update the count instantly and revert on error.
- Use Design Mode (Option+D) to style the public board and widget to match your product's brand colors for free.
- Set NEXT_PUBLIC_SUPABASE_ANON_KEY for the public board's client-side operations and SUPABASE_SERVICE_ROLE_KEY for the widget API.
- Set NEXT_PUBLIC_APP_URL to the production domain so widget iframe src URLs are correct after deployment.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a feedback collection tool with Next.js and Supabase. I need to create an embeddable widget that works on any website. Show me how to build a script tag that injects an iframe for the feedback form, use postMessage for cross-origin communication, and validate the Origin header in the API endpoint against a registered domain in the database.
Build the upvote toggle system for a feedback board. Create a 'use client' UpvoteButton component that shows an arrow icon and count. On click, call a Server Action that checks if an upvote exists for this user+feedback combo: if yes, delete it and decrement count; if no, insert and increment count. Use optimistic UI to update the count instantly. The unique constraint on (feedback_id, user_id) prevents race conditions.
Frequently asked questions
How does the embeddable widget work?
You add a script tag to your website that injects a small iframe pointing to your feedback form. The iframe is positioned as a floating button in the bottom-right corner. When users click it, the form expands. Submissions go through the /api/widget endpoint with Origin validation.
Can I prevent spam feedback from the widget?
Yes. The API validates the Origin header against the project's registered domain. You can also add rate limiting per IP address and require email verification for widget submissions.
How does upvoting work without creating an account?
Authenticated users can upvote with a unique(feedback_id, user_id) constraint preventing duplicates. For anonymous users, you can track upvotes by session ID stored in a cookie.
Can I customize the widget's appearance?
Yes. The widget page at /widget/[key] is a separate Next.js page you can style. Use Design Mode (Option+D) to adjust colors, fonts, and layout. Match your product's branding for a seamless experience.
How do I deploy the widget?
Publish to Vercel via V0's Share menu. Set NEXT_PUBLIC_APP_URL to your production domain in the Vars tab. Give customers the script tag with their unique widget_key to embed on their site.
Can RapidDev help build a custom feedback tool?
Yes. RapidDev has built 600+ apps including product feedback platforms with AI-powered categorization, Slack integrations, and public roadmap views. Book a free consultation to discuss your requirements.
What V0 plan do I need?
Premium ($20/month) is recommended for the widget and board components. Free tier can build the basic feedback list but may need manual coding for the embeddable widget system.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation