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

How to Build Online quiz app with V0

Build a timed quiz app with V0 using Next.js, Supabase, and shadcn/ui. Features multiple question types, a countdown timer, server-side answer scoring to prevent cheating, instant results, and a public leaderboard — all in about 30-60 minutes with no Stripe needed.

What you'll build

  • Quiz browser with Card grid showing title, question count, time limit, and difficulty Badge
  • Quiz taking interface with countdown timer, RadioGroup for multiple choice, and Progress bar
  • Server-side scoring API that validates answers against the database to prevent cheating
  • Results page with score breakdown, correct/incorrect indicators, and time taken
  • Leaderboard Table showing top scorers with Avatar, score Badge, and time ranking
  • Quiz builder form for creating quizzes with drag-and-drop question ordering
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read30-60 minutesV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a timed quiz app with V0 using Next.js, Supabase, and shadcn/ui. Features multiple question types, a countdown timer, server-side answer scoring to prevent cheating, instant results, and a public leaderboard — all in about 30-60 minutes with no Stripe needed.

What you're building

Teachers, trainers, and content creators need a way to test knowledge with instant feedback. A timed quiz app with automatic scoring keeps learners engaged and gives creators actionable data on what topics need more coverage.

V0 makes this a perfect beginner project — describe the quiz interface in chat, V0 generates the full component with timer and scoring UI, then use Design Mode (Option+D) to adjust styling for free. No Stripe or complex integrations needed.

The architecture uses a 'use client' component for the timer and question navigation, a Server Action for answer submission and scoring, and Supabase for storing quizzes, questions, and attempt results. Correct answers never leave the server.

Final result

A timed quiz application with multiple question types, anti-cheat server-side scoring, instant results with breakdowns, and a public leaderboard.

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)
  • Quiz content prepared (questions, answer options, correct answers)
  • Basic understanding of forms and state management in React

Build steps

1

Set up the database schema for quizzes and attempts

Create the Supabase schema for quizzes, questions with multiple types, attempt tracking, and individual answer storage for detailed results.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a timed quiz app. Create a Supabase schema:
3// 1. quizzes: id (uuid PK), title (text), description (text), time_limit_seconds (int), is_published (boolean DEFAULT false), creator_id (uuid FK to auth.users), created_at (timestamptz)
4// 2. questions: id (uuid PK), quiz_id (uuid FK to quizzes), question_text (text), type (text CHECK IN 'multiple_choice','true_false','short_answer'), options (jsonb), correct_answer (text), points (int DEFAULT 10), position (int)
5// 3. attempts: id (uuid PK), quiz_id (uuid FK to quizzes), user_id (uuid FK to auth.users), score (int), max_score (int), time_taken_seconds (int), completed_at (timestamptz)
6// 4. answers: id (uuid PK), attempt_id (uuid FK to attempts), question_id (uuid FK to questions), user_answer (text), is_correct (boolean), points_earned (int)
7// Add RLS so users can only read their own attempts. Generate SQL and types.

Pro tip: Use V0's beginner-friendly workflow — describe the quiz UI in chat, V0 generates it, then use Design Mode (Option+D) to tweak question Card colors and timer styling for free.

Expected result: All tables created with proper foreign keys and RLS policies that restrict attempt data to the quiz taker.

2

Build the quiz browser and taking interface

Create the quiz catalog showing available quizzes and the quiz-taking interface with a countdown timer, question navigation, and answer selection.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create quiz pages:
3// 1. app/quizzes/page.tsx — browse quizzes with Card grid: title, description, question count, time limit Badge, difficulty Badge. Add search Input and category filter.
4// 2. app/quiz/[id]/page.tsx — 'use client' quiz taking interface:
5// - Countdown timer at the top (useEffect with setInterval, auto-submits when timer hits 0)
6// - Progress bar showing question X of Y
7// - Question Card with RadioGroup for multiple choice, true/false toggle, or Input for short answer
8// - Previous/Next navigation Buttons
9// - Submit Button that sends all answers to the scoring API
10// - Store attempt start time in state for server-side time validation
11// Use shadcn/ui Card, RadioGroup, Progress, Badge, Button, Input.

Expected result: The quiz browser shows available quizzes. Clicking one starts the timer and displays questions one at a time with navigation controls.

3

Build the server-side scoring API

Create the scoring endpoint that receives answers, fetches correct answers from the database, computes the score server-side, and stores the attempt results. Never expose correct answers to the client.

app/api/quiz/submit/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 POST(req: NextRequest) {
10 const { quiz_id, user_id, answers, start_time } = await req.json()
11
12 const { data: questions } = await supabase
13 .from('questions')
14 .select('id, correct_answer, points')
15 .eq('quiz_id', quiz_id)
16 .order('position')
17
18 if (!questions) {
19 return NextResponse.json({ error: 'Quiz not found' }, { status: 404 })
20 }
21
22 const { data: quiz } = await supabase
23 .from('quizzes')
24 .select('time_limit_seconds')
25 .eq('id', quiz_id)
26 .single()
27
28 const timeTaken = Math.floor((Date.now() - new Date(start_time).getTime()) / 1000)
29 if (quiz && timeTaken > quiz.time_limit_seconds + 5) {
30 return NextResponse.json({ error: 'Time limit exceeded' }, { status: 400 })
31 }
32
33 let score = 0
34 const maxScore = questions.reduce((sum, q) => sum + q.points, 0)
35 const gradedAnswers = questions.map((q) => {
36 const userAnswer = answers[q.id] || ''
37 const isCorrect = userAnswer.toLowerCase() === q.correct_answer.toLowerCase()
38 const pointsEarned = isCorrect ? q.points : 0
39 score += pointsEarned
40 return {
41 question_id: q.id,
42 user_answer: userAnswer,
43 is_correct: isCorrect,
44 points_earned: pointsEarned,
45 }
46 })
47
48 const { data: attempt } = await supabase
49 .from('attempts')
50 .insert({ quiz_id, user_id, score, max_score: maxScore, time_taken_seconds: timeTaken })
51 .select('id')
52 .single()
53
54 if (attempt) {
55 await supabase.from('answers').insert(
56 gradedAnswers.map((a) => ({ ...a, attempt_id: attempt.id }))
57 )
58 }
59
60 return NextResponse.json({ attempt_id: attempt?.id, score, max_score: maxScore, time_taken: timeTaken })
61}

Expected result: Submitting answers sends them to the server where scoring happens. The client receives only the final score — correct answers are never exposed.

4

Build the results page, leaderboard, and deploy

Create the results breakdown page and a public leaderboard showing top scores for each quiz. Then deploy.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create results and leaderboard pages:
3// 1. app/quiz/[id]/results/page.tsx — score Card showing score/maxScore percentage, time taken, and a Table of each question with the user's answer, correct answer, and a green checkmark or red X icon.
4// 2. app/quiz/[id]/leaderboard/page.tsx — Table showing rank, user Avatar and name, score Badge, and time taken. Sorted by score DESC then time ASC. Highlight the current user's row.
5// 3. app/create/page.tsx — quiz builder form: Input for title, Textarea for description, Input for time limit. Add Question Button that appends a question form with question_text Input, type Select (multiple_choice/true_false/short_answer), dynamic option Inputs, and correct_answer Select. Reorder with drag handles.
6// Use shadcn/ui Table, Card, Badge, Avatar, Select, Button, Input, Textarea.

Pro tip: This is a Supabase-only project with no Stripe needed — set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in Vars and you are ready to deploy.

Expected result: Results show a detailed breakdown per question. The leaderboard ranks all participants. The quiz builder lets creators add new quizzes. The app is deployed.

Complete code

app/api/quiz/submit/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 POST(req: NextRequest) {
10 const { quiz_id, user_id, answers, start_time } = await req.json()
11
12 const { data: questions } = await supabase
13 .from('questions')
14 .select('id, correct_answer, points')
15 .eq('quiz_id', quiz_id)
16 .order('position')
17
18 if (!questions?.length) {
19 return NextResponse.json({ error: 'Quiz not found' }, { status: 404 })
20 }
21
22 const { data: quiz } = await supabase
23 .from('quizzes')
24 .select('time_limit_seconds')
25 .eq('id', quiz_id)
26 .single()
27
28 const elapsed = Math.floor(
29 (Date.now() - new Date(start_time).getTime()) / 1000
30 )
31
32 if (quiz && elapsed > quiz.time_limit_seconds + 5) {
33 return NextResponse.json(
34 { error: 'Time limit exceeded' },
35 { status: 400 }
36 )
37 }
38
39 let score = 0
40 const maxScore = questions.reduce((s, q) => s + q.points, 0)
41
42 const graded = questions.map((q) => {
43 const ua = answers[q.id] || ''
44 const correct =
45 ua.toLowerCase().trim() === q.correct_answer.toLowerCase().trim()
46 const pts = correct ? q.points : 0
47 score += pts
48 return {
49 question_id: q.id,
50 user_answer: ua,
51 is_correct: correct,
52 points_earned: pts,
53 }
54 })
55
56 const { data: attempt } = await supabase
57 .from('attempts')
58 .insert({
59 quiz_id,
60 user_id,
61 score,
62 max_score: maxScore,
63 time_taken_seconds: elapsed,
64 })
65 .select('id')
66 .single()
67
68 if (attempt) {
69 await supabase
70 .from('answers')
71 .insert(graded.map((a) => ({ ...a, attempt_id: attempt.id })))
72 }
73
74 return NextResponse.json({
75 attempt_id: attempt?.id,
76 score,
77 max_score: maxScore,
78 time_taken: elapsed,
79 })
80}

Customization ideas

Question image attachments

Add an image_url field to questions and display images alongside question text for visual quizzes like geography or art history.

Quiz categories and difficulty levels

Add category and difficulty fields to quizzes and build filtered browse pages so users can find quizzes by topic and skill level.

Streaks and achievements

Track consecutive quiz completions and award achievement badges for milestones like 'Perfect Score' or '10-Day Streak'.

Timed practice mode

Add a practice mode that shows correct answers after each question without recording scores, letting users study before attempting the real quiz.

Common pitfalls

Pitfall: Scoring quizzes on the client side

How to avoid: Score answers in an API route or Server Action. The client sends answer IDs; the server fetches correct answers from the database, computes the score, and stores results.

Pitfall: Not validating the timer server-side

How to avoid: Store the attempt start time in the database and compare it to submission time on the server. Add a 5-second grace period for network latency, then reject late submissions.

Pitfall: Using offset-based pagination for the leaderboard

How to avoid: Use a composite index on (quiz_id, score DESC, time_taken_seconds ASC) and cursor-based pagination keyed on score and time for fast leaderboard queries.

Best practices

  • Score quizzes server-side in an API route — never expose correct answers to the client
  • Validate the time limit server-side by comparing attempt start time with submission time
  • Use V0's Design Mode (Option+D) to style quiz Cards and timer components without spending credits
  • Store question options and correct answers in a JSONB field for flexible question types
  • Add a unique constraint on (quiz_id, user_id) in attempts if you want to limit to one attempt per user
  • Use RLS policies so users can only read their own attempt details but can see aggregate leaderboard scores
  • Randomize question order per attempt to reduce answer-sharing between users
  • Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in Vars — no Stripe needed for this project

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a timed quiz app with Next.js App Router and Supabase. When a user submits their answers, I need to score them server-side to prevent cheating. Please write the API route at app/api/quiz/submit/route.ts that receives the quiz_id, user_id, answers object, and start_time. It should fetch correct answers from the database, validate the time limit, compute the score, store the attempt and individual answers, and return the results.

Build Prompt

Create a countdown timer component for a quiz app. It receives timeLimit (seconds) and onTimeUp callback. Show minutes:seconds in a large Badge. Use red text when under 30 seconds. Auto-call onTimeUp when timer reaches 0. Use useEffect with setInterval, cleanup on unmount. Include a visual Progress bar that depletes as time runs out.

Frequently asked questions

How does the anti-cheat scoring work?

When a user submits their answers, the client sends only the answer selections (not correct answers). The server fetches correct answers from the database, compares them, computes the score, and returns results. Correct answers are never included in the client bundle or API responses.

Can I add different question types?

Yes. The questions table has a type field supporting multiple_choice, true_false, and short_answer. The quiz-taking component renders RadioGroup for multiple choice, a toggle for true/false, or an Input for short answer. Add new types by extending the type check and adding corresponding UI components.

How is the timer enforced?

The timer is enforced on both client and server. The client shows a countdown and auto-submits at zero. The server compares the attempt start time with the submission timestamp and rejects submissions that exceed the time limit plus a 5-second grace period.

Do I need a paid V0 plan?

No. The quiz app is simple enough to build with the V0 free tier. It requires only a few prompts for the quiz interface, scoring API, and leaderboard. No Stripe integration is needed.

How does the leaderboard work?

The leaderboard queries attempts for a specific quiz, sorted by score descending and time ascending (faster completions rank higher at the same score). It displays rank, user name, score, and time taken in a shadcn/ui Table.

How do I deploy the quiz app?

Click Share in V0, then Publish to Production. Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars tab. No Stripe or other external services needed.

Can RapidDev help build a custom quiz platform?

Yes. RapidDev has built over 600 apps including assessment platforms with proctoring, adaptive difficulty, and analytics dashboards. Book a free consultation to discuss your quiz or testing needs.

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.