# How to Add a Knowledge Quiz to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A knowledge quiz needs a React state machine managing question flow and scoring, server-side answer validation so correct answers are never exposed to the client, and a Supabase table for results and leaderboard. With V0 or Lovable you can ship a working quiz with a timer, score reveal animation, and leaderboard in 4–8 hours for $0–$25/month. All rendering libraries (Recharts, Framer Motion, canvas-confetti) are free.

## What an Interactive Knowledge Quiz Actually Is

An interactive knowledge quiz presents a sequence of questions, collects answers one at a time, provides immediate feedback on each selection, and delivers a scored result at the end. A SaaS onboarding quiz tests whether users understand a feature before unlocking it. A learning platform assesses topic mastery and suggests resources for missed questions. A community app runs weekly trivia with a live leaderboard. The architecture is the same across all three: a client-side state machine that drives the question sequence, server-side answer validation to prevent cheating (correct answers must never travel to the browser), and a Supabase results table that powers both the user's history and the public leaderboard.

## Anatomy of the Feature

Six components — the state machine and server-side validation are where first builds most commonly break. The score reveal and share widget add polish that dramatically increases completion and re-play rates.

- **Question renderer** (ui): A React component rendering question text and answer options as clickable cards or a RadioGroup. Framer Motion AnimatePresence handles slide-in transitions between questions. shadcn/ui Card and RadioGroup provide the base styling. After an answer is selected, the correct option highlights green and the wrong selected option highlights red before advancing.
- **Quiz state machine** (ui): A React useReducer managing the full quiz lifecycle: { currentIndex, answers, score, timeRemaining, status: 'idle | in-progress | reviewing | complete' }. Transitions: START_QUIZ, ANSWER_QUESTION, NEXT_QUESTION, COMPLETE_QUIZ, START_REVIEW. The timer runs in a useEffect + setInterval and dispatches a TIMEOUT action when timeRemaining reaches zero.
- **Score calculator** (data): For simple quizzes: client-side scoring compares submitted answers against correct answers returned after quiz completion. For cheating-resistant scoring: a Supabase Edge Function receives the user's answers and validates them against server-stored correct_answer values — the correct answers are never sent to the client in the questions query.
- **Results + breakdown page** (ui): Score reveal shows the percentage, a pass/fail label against the quiz's pass_score_pct, and a Recharts BarChart or RadarChart breaking down performance by topic/category. canvas-confetti triggers a 3-second animation on passing scores. The wrong-answer review list renders each question, the user's selected answer, the correct answer, and the explanation text.
- **Leaderboard** (backend): A Supabase query on the quiz_results table ordering by score DESC and limiting to 10 rows. An optional Supabase Realtime subscription updates the leaderboard live as new results are inserted. Each row shows rank, username, score, and completion time.
- **Share widget** (ui): The Web Share API (navigator.share) on mobile browsers opens the native share sheet. On desktop, a clipboard copy button saves a URL like /quiz/results?score=85&quiz=trivia-week-12. In Next.js, Vercel OG generates a score card image for social sharing using the quiz name and score as URL parameters.

## Data model

Three tables: quizzes stores quiz metadata, questions stores questions with correct answers (column excluded from public reads by RLS), and quiz_results stores all submitted scores. Run this in the Supabase SQL editor:

```sql
create table public.quizzes (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  description text,
  time_limit_seconds int,
  pass_score_pct int not null default 70,
  is_public boolean not null default true,
  created_at timestamptz not null default now()
);

create table public.questions (
  id uuid primary key default gen_random_uuid(),
  quiz_id uuid references public.quizzes(id) on delete cascade not null,
  text text not null,
  type text not null default 'multiple_choice' check (type in ('multiple_choice', 'true_false', 'short_answer')),
  options jsonb not null default '[]',
  correct_answer text not null,
  explanation text,
  category text,
  sort_order int not null default 0
);

create table public.quiz_results (
  id uuid primary key default gen_random_uuid(),
  quiz_id uuid references public.quizzes(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete set null,
  score int not null,
  total_questions int not null,
  answers_taken jsonb not null default '{}',
  passed boolean not null default false,
  completed_at timestamptz not null default now()
);

create index quiz_results_leaderboard_idx
  on public.quiz_results (quiz_id, score desc, completed_at asc);

create index quiz_results_user_idx
  on public.quiz_results (user_id, completed_at desc);

alter table public.quizzes enable row level security;
alter table public.questions enable row level security;
alter table public.quiz_results enable row level security;

create policy "Public can read public quizzes"
  on public.quizzes for select
  using (is_public = true);

create policy "Public can read questions without correct answers"
  on public.questions for select
  using (true);

create policy "Users can insert own results"
  on public.quiz_results for insert
  with check (auth.uid() = user_id);

create policy "Users can read own results"
  on public.quiz_results for select
  using (auth.uid() = user_id);

create policy "Public can read quiz results for leaderboard"
  on public.quiz_results for select
  using (true);
```

IMPORTANT: The questions SELECT policy allows public access to the questions table, but you must exclude the correct_answer column in your client query — use `select('id, text, type, options, explanation, category, sort_order')` and never include correct_answer. Answer validation happens exclusively in a Supabase Edge Function that uses the service role key to read correct_answer values. This is the only architecture that prevents browser DevTools from revealing answers.

## Build paths

### Lovable — fit 4/10, 4–6 hours

Lovable handles React state machines and Supabase wiring well. Framer Motion transitions and canvas-confetti score reveal work in the Vite/React stack. Good for a quiz embedded in a Lovable-built app.

1. Create a Lovable project and connect Lovable Cloud to auto-provision Supabase with auth
2. Run the SQL schema from this page in the Supabase SQL editor — the questions table setup and RLS policies must be in place before testing
3. Paste the prompt below into Agent Mode; it generates the full quiz flow, results page, and leaderboard
4. After generation, verify in the Supabase Dashboard that the public questions SELECT policy does NOT include correct_answer in the allowed columns — this is the most critical security check
5. Test the quiz on the published URL (not the preview iframe) to confirm score sharing and the Web Share API work correctly on mobile

Starter prompt:

```
Build an interactive knowledge quiz. Quiz flow: fetch quiz metadata and questions from Supabase 'quizzes' and 'questions' tables — never fetch the correct_answer column in the client query. Display one question at a time with answer options as clickable cards using a useReducer state machine tracking { currentIndex, answers: {}, score, timeRemaining, status }. Timer: show a countdown from quiz.time_limit_seconds in the header; change color to red when under 10 seconds; auto-submit when timer reaches zero. After each answer: lock further clicks, highlight the selected option (green if correct, red if wrong — validated via a Supabase Edge Function POST /validate-answer that accepts question_id and selected_answer and returns { correct: boolean, correct_answer, explanation }), show the explanation text for 2 seconds, then advance to the next question automatically. Progress bar: show 'Question N of Total' and a filled progress bar. Results page: show score as a percentage and a pass/fail label against the quiz pass_score_pct; render a Recharts BarChart of correct vs incorrect by category; trigger canvas-confetti on pass; show a wrong-answer review list. Leaderboard: query top 10 quiz_results for this quiz ordered by score DESC. Share button: copy /quiz/[quiz-id]/results?score=[score] to clipboard; try navigator.share first on mobile. Store result in quiz_results table on completion. Handle the case where the user navigates away mid-quiz: show a browser beforeunload warning 'Progress will be lost if you leave this page'.
```

Limitations:

- Correct answers must be validated server-side in a Supabase Edge Function — verify Lovable did not generate a client-side comparison against fetched correct_answer values after generation
- Vercel OG score card images for social sharing are not available in Lovable's Vite/React stack; clipboard copy of the score URL is the fallback
- Supabase Realtime leaderboard subscription may need manual wiring if Lovable generates a static fetch instead

### V0 — fit 5/10, 4–6 hours

V0 produces the highest UI quality for web quizzes — polished card layouts, Recharts score breakdowns, shadcn/ui RadioGroups. Next.js Server Actions validate answers securely server-side without a separate Edge Function. OG image generation for score sharing is native to Next.js.

1. Prompt V0 with the spec below to generate the quiz page, results page, and leaderboard component
2. Add Supabase environment variables in the V0 Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY (for the Server Action answer validation)
3. Run the SQL schema from this page in the Supabase SQL editor
4. Verify the Server Action that validates answers uses the service role Supabase client, not the anon client, to read correct_answer values
5. Add an /api/og route for the score card OG image using Vercel's @vercel/og package with the quiz name and score as parameters
6. Connect to GitHub via the V0 Git panel and deploy to Vercel to test OG image generation and the Web Share API on a real device

Starter prompt:

```
Build a Next.js interactive knowledge quiz at /quiz/[quizId]. Quiz page (client component): fetch quiz and questions from Supabase using the anon client — select only id, text, type, options, explanation, category, sort_order from questions (never correct_answer). Use useReducer for state: { currentIndex, answers: Record<string, string>, score: number, timeRemaining: number, status: 'idle' | 'in-progress' | 'reviewing' | 'complete' }. Timer: useEffect + setInterval decrementing timeRemaining each second; clear on unmount; auto-call submitQuiz action when it hits 0. One question at a time with AnimatePresence from framer-motion (mode='wait', key={currentIndex}). Answer options as shadcn/ui Button cards in a 2-column grid. On answer click: call a Next.js Server Action validateAnswer(questionId, selectedAnswer) that uses the Supabase service role client to read the correct_answer and return { correct: boolean, correctAnswer: string, explanation: string }. Highlight correct green, wrong red; show explanation for 2 seconds; then dispatch NEXT_QUESTION. Progress bar: shadcn/ui Progress component showing currentIndex/total. Results page /quiz/[quizId]/results: score %, pass/fail badge, Recharts BarChart of correct vs incorrect count by question.category, canvas-confetti effect on pass, wrong-answer review accordion. Leaderboard: top 10 quiz_results from Supabase ordered by score DESC then completed_at ASC. Share: navigator.share with title+score URL; fallback to clipboard. OG image: /api/og?quiz=[title]&score=[pct] using @vercel/og. Insert quiz_results row via Server Action on completion. Add beforeunload listener warning 'Your progress will be lost'.
```

Limitations:

- V0 will not auto-provision the Supabase tsvector or Realtime subscription — run the schema and configure Realtime manually
- Leaderboard Realtime updates need explicit prompt instruction; V0 defaults to a static fetch without a subscription
- OG image route requires a separate Vercel deployment to work — it cannot be tested in V0's sandbox preview

### Custom — fit 3/10, 3–5 days

Custom development is justified only for adaptive difficulty quizzes, anti-cheat proctoring, or LMS integration (SCORM/xAPI). For standard scored quizzes, V0 is faster and produces equivalent quality.

1. Next.js App Router with Server Actions for answer validation; Supabase for quiz storage and results; Recharts for the score breakdown visualization
2. Adaptive difficulty: track item difficulty ratings per question and use IRT (Item Response Theory) scoring to adjust which question is shown next based on accuracy rate
3. Anti-cheat: Intersection Observer detects when the quiz tab loses focus; a counter triggers a warning at 3 tab switches and auto-submits at 5
4. LMS integration: xAPI (Tin Can) statements sent to a Learning Record Store (LRS) on quiz completion with score, duration, and pass/fail outcome

Limitations:

- IRT/adaptive difficulty requires a question bank of 50+ questions with pre-calibrated difficulty ratings — significant content creation overhead
- SCORM packaging and LMS compatibility testing adds 1–2 days to the timeline and requires access to the target LMS for testing

## Gotchas

- **Users can open browser DevTools and see correct answers in the network response** — AI tools default to fetching the full questions row — including the correct_answer column — in the client query. This means any user who opens the Network tab in DevTools can read all correct answers before the quiz starts. The quiz integrity is completely compromised. Fix: In the Supabase client query, explicitly select only safe columns: `select('id, text, type, options, explanation, category, sort_order')`. Move answer validation to a Supabase Edge Function or Next.js Server Action that reads correct_answer using the service role key — the service role key is never exposed to the browser. After generation, verify the client-side network calls do not include correct_answer in any response.
- **Timer keeps running after quiz submission, causing a React state update on unmounted component error** — The setInterval timer is started in a useEffect but the cleanup function is missing or incomplete. When the quiz completes and the component unmounts (or navigates to the results page), the interval keeps firing and tries to update state on the now-unmounted component. React throws a warning and in some cases the results page receives incorrect time data. Fix: Return a cleanup function from the timer useEffect: `return () => clearInterval(timerId)`. Additionally, create a local `isActive` ref (useRef(true)) and check `if (isActive.current)` before calling dispatch in the interval callback. Set `isActive.current = false` in the cleanup. Both guards are needed.
- **Score sharing link shows 'undefined' in the OG image** — V0 or Lovable generates a share URL that reads the score from a URL search parameter, but the parameter is not URL-encoded before being appended to the URL. When the score contains special characters (or when the OG route receives an empty param), it renders 'undefined' in the social share card image. Fix: Encode the score and quiz name when building the share URL: `/api/og?quiz=${encodeURIComponent(quizTitle)}&score=${score}`. In the OG route handler, decode with `decodeURIComponent(searchParams.get('quiz') ?? '')` and add a fallback for null values before rendering.
- **Framer Motion question transitions cause layout shift — the next question jumps instead of sliding** — AnimatePresence requires two specific configurations to produce a smooth slide-in transition: a stable key prop on the animated element and mode='wait' on the AnimatePresence wrapper. Without mode='wait', the exiting question and entering question render simultaneously, causing a content overlap. Without a changing key, AnimatePresence doesn't detect that the content changed and skips the animation entirely. Fix: Add key={currentQuestionIndex} to the question wrapper div inside AnimatePresence, and mode='wait' to the AnimatePresence component itself. The complete structure: `<AnimatePresence mode='wait'><motion.div key={currentQuestionIndex} initial={{ x: 100, opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: -100, opacity: 0 }}>`.

## Best practices

- Never send the correct_answer column to the client — validate all answers in a Server Action or Edge Function using the service role key, even for quizzes where cheating seems unlikely
- Lock answer options after the first click by setting an isAnswered boolean in the question component state — allow one selection per question, no going back
- Show the explanation text for every answer (correct and incorrect) before advancing — explanations are what makes a quiz feel educational rather than just a scoring exercise
- Add a beforeunload event listener during the quiz to warn users their progress will be lost if they navigate away — especially important for timed quizzes
- Cache the questions array for a quiz session after the initial fetch — re-fetching on each question navigation adds unnecessary latency and Supabase read counts
- Build the wrong-answer review mode before launch — it is the most-requested feature after users complete a quiz and the primary reason they share results with others
- For public quizzes, add a quiz result INSERT trigger that updates a cached top-10 leaderboard table rather than computing ORDER BY score DESC on every leaderboard view

## Frequently asked questions

### How do I stop users from cheating by inspecting the quiz answers in the browser?

Never include the correct_answer column in your client-side Supabase query. When the user submits an answer, call a Next.js Server Action or Supabase Edge Function that reads the correct answer using the service role key (which never leaves the server) and returns only a boolean correct/incorrect plus the explanation. The correct answer text is only revealed after it's too late to use it.

### Can I add a timer to each quiz question in Lovable or V0?

Yes. Both tools can generate a useEffect timer that decrements a timeRemaining value in the quiz state machine. For per-question timers, reset timeRemaining to the question's allotted seconds every time currentIndex changes. For a total quiz timer, start it once at quiz load and auto-submit when it hits zero. Explicitly describe this behavior in your prompt — AI tools default to no timer.

### How do I show correct answers after the quiz is submitted?

Build a review mode state ('reviewing') in your quiz state machine. When the quiz completes, the Edge Function or Server Action returns the correct answers for all questions as part of the final result payload. In review mode, render each question with the user's answer highlighted and the correct answer shown — safe to display client-side after submission since the quiz is over.

### Can I add a leaderboard to a knowledge quiz?

Yes. Insert a row into quiz_results on completion with the user's score. Query top 10 rows ordered by score DESC for the leaderboard display. For a live leaderboard that updates as new scores come in, add a Supabase Realtime subscription on the quiz_results table filtered by quiz_id.

### How do I let users share their quiz score on social media?

Two approaches: the Web Share API (navigator.share) opens the native share sheet on mobile with a title, description, and URL — check navigator.share !== undefined before calling it and fall back to clipboard copy on desktop. For rich social preview cards, add a Next.js /api/og route using @vercel/og that renders a score card image when the share URL is pasted into Twitter, Slack, or iMessage.

### What's the best way to store quiz results in Supabase?

A quiz_results table with quiz_id, user_id, score, total_questions, passed (boolean), answers_taken (jsonb storing each question_id and the user's selected answer), and completed_at. Add a composite index on (quiz_id, score DESC) for fast leaderboard queries. Insert via a Server Action or Edge Function that also validates the final answers — never via a direct client insert, which would allow score manipulation.

### Can I build a quiz with different question types — multiple choice and true/false?

Yes. Store the question type as a 'type' column ('multiple_choice', 'true_false', 'short_answer') and the answer options as a jsonb array. In the question renderer, use a conditional switch on type to render the appropriate input: RadioGroup cards for multiple choice, a two-option True/False toggle for boolean, or a text input for short answer. Server-side answer comparison handles each type's matching logic.

---

Source: https://www.rapidevelopers.com/app-features/interactive-knowledge-quiz
© RapidDev — https://www.rapidevelopers.com/app-features/interactive-knowledge-quiz
