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
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
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.
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.
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.
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 Y7// - Question Card with RadioGroup for multiple choice, true/false toggle, or Input for short answer8// - Previous/Next navigation Buttons9// - Submit Button that sends all answers to the scoring API10// - Store attempt start time in state for server-side time validation11// 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.
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.
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { quiz_id, user_id, answers, start_time } = await req.json()1112 const { data: questions } = await supabase13 .from('questions')14 .select('id, correct_answer, points')15 .eq('quiz_id', quiz_id)16 .order('position')1718 if (!questions) {19 return NextResponse.json({ error: 'Quiz not found' }, { status: 404 })20 }2122 const { data: quiz } = await supabase23 .from('quizzes')24 .select('time_limit_seconds')25 .eq('id', quiz_id)26 .single()2728 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 }3233 let score = 034 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 : 039 score += pointsEarned40 return {41 question_id: q.id,42 user_answer: userAnswer,43 is_correct: isCorrect,44 points_earned: pointsEarned,45 }46 })4748 const { data: attempt } = await supabase49 .from('attempts')50 .insert({ quiz_id, user_id, score, max_score: maxScore, time_taken_seconds: timeTaken })51 .select('id')52 .single()5354 if (attempt) {55 await supabase.from('answers').insert(56 gradedAnswers.map((a) => ({ ...a, attempt_id: attempt.id }))57 )58 }5960 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.
Build the results page, leaderboard, and deploy
Create the results breakdown page and a public leaderboard showing top scores for each quiz. Then deploy.
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
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { quiz_id, user_id, answers, start_time } = await req.json()1112 const { data: questions } = await supabase13 .from('questions')14 .select('id, correct_answer, points')15 .eq('quiz_id', quiz_id)16 .order('position')1718 if (!questions?.length) {19 return NextResponse.json({ error: 'Quiz not found' }, { status: 404 })20 }2122 const { data: quiz } = await supabase23 .from('quizzes')24 .select('time_limit_seconds')25 .eq('id', quiz_id)26 .single()2728 const elapsed = Math.floor(29 (Date.now() - new Date(start_time).getTime()) / 100030 )3132 if (quiz && elapsed > quiz.time_limit_seconds + 5) {33 return NextResponse.json(34 { error: 'Time limit exceeded' },35 { status: 400 }36 )37 }3839 let score = 040 const maxScore = questions.reduce((s, q) => s + q.points, 0)4142 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 : 047 score += pts48 return {49 question_id: q.id,50 user_answer: ua,51 is_correct: correct,52 points_earned: pts,53 }54 })5556 const { data: attempt } = await supabase57 .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()6768 if (attempt) {69 await supabase70 .from('answers')71 .insert(graded.map((a) => ({ ...a, attempt_id: attempt.id })))72 }7374 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation