Skip to main content
RapidDev - Software Development Agency
how-to-build-v030-60 minutes

How to Build Polls and surveys with V0

Build a polls and surveys app with V0 using Next.js and Supabase that lets you create questions, share via link, and see real-time results as votes come in. Features multiple question types, duplicate vote prevention, and live result visualization — all in about 30-60 minutes.

What you'll build

  • Survey builder with single-choice, multiple-choice, text, and rating question types
  • Public response form shareable via link with no login required for respondents
  • Real-time results page with shadcn/ui Progress bars updating as votes arrive
  • Duplicate vote prevention using anonymous fingerprinting stored in localStorage
  • Survey management dashboard listing all surveys with published/draft status Badges
  • Server Actions for creating surveys and submitting responses without API routes
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read30-60 minutesV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a polls and surveys app with V0 using Next.js and Supabase that lets you create questions, share via link, and see real-time results as votes come in. Features multiple question types, duplicate vote prevention, and live result visualization — all in about 30-60 minutes.

What you're building

Polls and surveys are essential for gathering feedback, validating ideas, and engaging audiences. Whether you are running a product survey, a team vote, or a community poll, you need a tool that is easy to create, simple to share, and shows results instantly.

V0 makes building this straightforward — describe the survey builder and response form in chat, and V0 generates the Next.js pages, form components, and Server Actions. Connect Supabase via the Connect panel for instant database provisioning, and your poll app is live in under an hour.

The architecture uses Next.js App Router with Server Components for the survey builder and results pages, a client component for the interactive response form, Server Actions for all mutations, and Supabase for storing surveys, questions, and responses with RLS policies for access control.

Final result

A complete polls and surveys application with a drag-and-drop question builder, public shareable response forms, real-time result visualization with progress bars, and duplicate vote prevention.

Tech stack

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

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • An idea for your first poll or survey to test with

Build steps

1

Set up the project and database schema

Open V0 and create a new project. Use the Connect panel to add Supabase — this auto-provisions your database credentials. Then prompt V0 to create the schema for surveys, questions, and responses.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a polls and surveys app. Create a Supabase schema with these tables:
3// 1. surveys: id (uuid PK), creator_id (uuid FK to auth.users), title (text), description (text), type (text check poll/survey), is_published (boolean default false), closes_at (timestamptz), created_at (timestamptz)
4// 2. questions: id (uuid PK), survey_id (uuid FK), question_text (text), question_type (text check single/multiple/text/rating), options (jsonb), position (integer), created_at (timestamptz)
5// 3. responses: id (uuid PK), survey_id (uuid FK), question_id (uuid FK), respondent_id (uuid), answer (jsonb), created_at (timestamptz) with unique constraint on (question_id, respondent_id)
6// Add RLS: anyone can INSERT responses on published surveys, only creator can SELECT all responses.
7// Generate SQL migration and TypeScript types.

Pro tip: Use V0's Design Mode (Option+D) after generating the survey form to visually adjust spacing, font sizes, and colors for free — no credits spent.

Expected result: Supabase is connected via the Connect panel with surveys, questions, and responses tables created. RLS policies allow public response submission but restrict result viewing to the survey creator.

2

Build the survey creation form

Prompt V0 to generate a survey builder page where users add questions with different types. Each question type gets a different input component — RadioGroup for single-choice, Checkbox for multiple-choice, Textarea for text, and a star rating component.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a survey builder at app/surveys/[id]/page.tsx.
3// Requirements:
4// - Fetch the survey and its questions from Supabase
5// - Display each question in a shadcn/ui Card with the question text and type Badge
6// - "Add Question" Button opens a Dialog with: Input for question text, Select for question_type (single/multiple/text/rating), dynamic options editor for choice questions (add/remove option inputs)
7// - Questions are reorderable by position number
8// - "Publish" Button with Switch toggle sets is_published to true via Server Action
9// - Use Separator between questions
10// - Server Actions: createSurvey(), addQuestion(), publishSurvey()
11// - Server Components for data fetching, 'use client' for the interactive form elements

Expected result: A survey builder page where you add questions of different types, configure options for choice questions, reorder them, and publish the survey with a single toggle.

3

Create the public response form

Build the page respondents see when they open a shared survey link. This must work without authentication — anyone with the link can respond. The form renders different input components based on question type and prevents duplicate submissions.

components/survey-response-form.tsx
1'use client'
2
3import { useState, useEffect } from 'react'
4import { Button } from '@/components/ui/button'
5import { Card } from '@/components/ui/card'
6import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
7import { Checkbox } from '@/components/ui/checkbox'
8import { Textarea } from '@/components/ui/textarea'
9import { Label } from '@/components/ui/label'
10import { submitResponse } from '@/app/actions/surveys'
11
12interface Question {
13 id: string
14 question_text: string
15 question_type: 'single' | 'multiple' | 'text' | 'rating'
16 options: string[] | null
17}
18
19export function SurveyResponseForm({
20 surveyId,
21 questions,
22}: {
23 surveyId: string
24 questions: Question[]
25}) {
26 const [answers, setAnswers] = useState<Record<string, unknown>>({})
27 const [respondentId, setRespondentId] = useState<string>('')
28 const [submitted, setSubmitted] = useState(false)
29
30 useEffect(() => {
31 const stored = localStorage.getItem(`survey-${surveyId}-respondent`)
32 if (stored) {
33 setSubmitted(true)
34 return
35 }
36 const id = crypto.randomUUID()
37 setRespondentId(id)
38 }, [surveyId])
39
40 async function handleSubmit() {
41 for (const question of questions) {
42 await submitResponse({
43 survey_id: surveyId,
44 question_id: question.id,
45 respondent_id: respondentId,
46 answer: answers[question.id],
47 })
48 }
49 localStorage.setItem(`survey-${surveyId}-respondent`, respondentId)
50 setSubmitted(true)
51 }
52
53 if (submitted) return <p>Thank you for your response!</p>
54
55 return (
56 <div className="space-y-6">
57 {questions.map((q) => (
58 <Card key={q.id} className="p-6">
59 <Label className="text-lg font-medium">{q.question_text}</Label>
60 {q.question_type === 'single' && q.options && (
61 <RadioGroup onValueChange={(v) => setAnswers({ ...answers, [q.id]: v })}>
62 {q.options.map((opt) => (
63 <div key={opt} className="flex items-center space-x-2">
64 <RadioGroupItem value={opt} id={`${q.id}-${opt}`} />
65 <Label htmlFor={`${q.id}-${opt}`}>{opt}</Label>
66 </div>
67 ))}
68 </RadioGroup>
69 )}
70 {q.question_type === 'text' && (
71 <Textarea
72 onChange={(e) => setAnswers({ ...answers, [q.id]: e.target.value })}
73 placeholder="Type your answer..."
74 />
75 )}
76 </Card>
77 ))}
78 <Button onClick={handleSubmit} size="lg">Submit Response</Button>
79 </div>
80 )
81}

Expected result: A public form at /surveys/[id]/respond that renders each question with the appropriate input type. After submission, localStorage prevents duplicate votes and shows a thank-you message.

4

Build the live results page with progress bars

Create a results page that shows vote counts and percentages for each question. For choice questions, display results as Progress bars. The page uses Server Components for initial data and can be refreshed to show updated results.

app/surveys/[id]/results/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Card } from '@/components/ui/card'
3import { Progress } from '@/components/ui/progress'
4import { Badge } from '@/components/ui/badge'
5import { Separator } from '@/components/ui/separator'
6
7export default async function ResultsPage({
8 params,
9}: {
10 params: Promise<{ id: string }>
11}) {
12 const { id } = await params
13 const supabase = await createClient()
14
15 const { data: questions } = await supabase
16 .from('questions')
17 .select('id, question_text, question_type, options')
18 .eq('survey_id', id)
19 .order('position')
20
21 const { data: responses } = await supabase
22 .from('responses')
23 .select('question_id, answer')
24 .eq('survey_id', id)
25
26 return (
27 <div className="max-w-2xl mx-auto p-6 space-y-8">
28 {questions?.map((q) => {
29 const qResponses = responses?.filter((r) => r.question_id === q.id) ?? []
30 const total = qResponses.length
31
32 return (
33 <Card key={q.id} className="p-6">
34 <h3 className="text-lg font-semibold mb-1">{q.question_text}</h3>
35 <Badge variant="outline" className="mb-4">{total} responses</Badge>
36 {q.question_type === 'single' && q.options?.map((opt: string) => {
37 const count = qResponses.filter((r) => r.answer === opt).length
38 const pct = total > 0 ? Math.round((count / total) * 100) : 0
39 return (
40 <div key={opt} className="mb-3">
41 <div className="flex justify-between text-sm mb-1">
42 <span>{opt}</span>
43 <span>{pct}% ({count})</span>
44 </div>
45 <Progress value={pct} />
46 </div>
47 )
48 })}
49 <Separator className="mt-4" />
50 </Card>
51 )
52 })}
53 </div>
54 )
55}

Pro tip: Use V0's Design Mode to visually adjust the Progress bar colors and Card spacing to match your brand — completely free, no credits consumed.

Expected result: The results page shows each question with vote counts and Progress bars. Single-choice questions display option-by-option percentages. The page renders with Server Components for fast initial load.

Complete code

app/actions/surveys.ts
1'use server'
2
3import { createClient } from '@/lib/supabase/server'
4import { revalidatePath } from 'next/cache'
5
6export async function createSurvey(formData: FormData) {
7 const supabase = await createClient()
8 const { data: { user } } = await supabase.auth.getUser()
9
10 const { data, error } = await supabase
11 .from('surveys')
12 .insert({
13 creator_id: user?.id,
14 title: formData.get('title') as string,
15 description: formData.get('description') as string,
16 type: formData.get('type') as string,
17 })
18 .select()
19 .single()
20
21 if (error) throw new Error(error.message)
22 revalidatePath('/surveys')
23 return data
24}
25
26export async function submitResponse(input: {
27 survey_id: string
28 question_id: string
29 respondent_id: string
30 answer: unknown
31}) {
32 const supabase = await createClient()
33
34 const { error } = await supabase.from('responses').insert({
35 survey_id: input.survey_id,
36 question_id: input.question_id,
37 respondent_id: input.respondent_id,
38 answer: input.answer,
39 })
40
41 if (error && error.code !== '23505') {
42 throw new Error(error.message)
43 }
44
45 revalidatePath(`/surveys/${input.survey_id}/results`)
46}

Customization ideas

Add Supabase Realtime for live results

Subscribe to the responses table using Supabase Realtime so the results page updates instantly as new votes come in, without manual page refresh.

Add survey expiration with countdown

Use the closes_at field to automatically disable the response form when the deadline passes, showing a countdown timer component until closure.

Add CSV export for responses

Create a Server Action that queries all responses for a survey and generates a downloadable CSV file for analysis in spreadsheet tools.

Add conditional logic between questions

Implement skip logic where answering a specific option on one question shows or hides follow-up questions, creating branching survey flows.

Common pitfalls

Pitfall: Accessing localStorage directly in a Server Component to check for duplicate votes

How to avoid: Wrap the duplicate check in a 'use client' component and access localStorage inside useEffect, which only runs in the browser.

Pitfall: Not adding a unique constraint on (question_id, respondent_id) for vote deduplication

How to avoid: Add a unique constraint in your Supabase schema and handle the 23505 duplicate key error gracefully in your Server Action.

Pitfall: Requiring authentication for respondents to submit votes

How to avoid: Keep the response form public with no auth required. Use anonymous fingerprinting (UUID in localStorage) for duplicate prevention instead.

Best practices

  • Use Server Actions for all mutations (creating surveys, submitting responses) — no API routes needed for this project
  • Use V0's Design Mode (Option+D) to visually adjust the survey form layout and result charts without spending credits
  • Set RLS policies that allow public INSERT on responses but restrict SELECT to the survey creator
  • Handle the Supabase 23505 duplicate key error gracefully to prevent duplicate vote error messages
  • Use shadcn/ui Skeleton components while loading results to provide visual feedback during data fetching
  • Store the respondent UUID in localStorage only after successful submission to prevent premature blocking

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a polls and surveys app with Next.js App Router and Supabase. Help me design the database schema for supporting single-choice, multiple-choice, open text, and rating question types. I need to prevent duplicate votes from the same person without requiring login. How should I structure the responses table and what constraints should I add?

Build Prompt

Create a real-time poll results component. Use Supabase Realtime to subscribe to new responses and update vote counts live. Show each option as a shadcn/ui Progress bar with percentage labels. Include a Badge showing total response count. The component should be 'use client' with useEffect for the Realtime subscription.

Frequently asked questions

Can I build this poll app on V0's free tier?

Yes. The free tier gives you enough credits to generate the survey builder, response form, and results page. Supabase free tier handles the database. You only need a paid plan if you want more complex features or faster generation.

How do I prevent people from voting multiple times?

The build uses anonymous fingerprinting — a UUID stored in localStorage when a user submits their first response. Combined with a unique constraint on (question_id, respondent_id) in Supabase, this prevents duplicate votes without requiring login.

Can I share surveys publicly without requiring respondents to sign up?

Yes. The response form at /surveys/[id]/respond is a public page with no authentication required. RLS policies allow anyone to INSERT responses on published surveys while restricting result viewing to the survey creator.

How do I see results update in real time?

The base build uses Server Components that show results on page load. For live updates, add Supabase Realtime subscriptions in a 'use client' component that listens for new inserts on the responses table and updates the UI immediately.

How do I deploy my polls app to production?

Click Share then Publish to Production in V0 — it deploys to Vercel in 30-60 seconds. Your Supabase credentials are automatically configured from the Connect panel. Share the survey URL from your Vercel domain.

Can RapidDev help build a custom polls and surveys platform?

Yes. RapidDev has built 600+ apps including survey platforms with advanced features like conditional logic, branching, and analytics dashboards. Book a free consultation to discuss your specific 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.