Build a full online course platform with V0 using Next.js, Supabase, Stripe, and shadcn/ui. Features structured courses with video lessons, interactive quiz modules, progress tracking, completion certificates via PDF generation, and Stripe-powered course purchases. Takes about 1-2 hours to complete.
What you're building
Educators and training companies need a platform for hosting structured courses with video lessons, quizzes, and completion certificates. Commercial LMS platforms charge per-student fees that eat into margins.
V0 generates the course catalog, lesson viewer, and quiz components from prompts. Stripe via the Vercel Marketplace handles course purchases, and Supabase stores course content and progress tracking.
The architecture uses Server Components for SEO-friendly course landing pages, Stripe Checkout for payment, webhook handlers for enrollment creation, and Server Actions for lesson completion and quiz scoring.
Final result
An online education platform with course purchases, structured lessons, quizzes, progress tracking, and PDF completion certificates.
Tech stack
Prerequisites
- A V0 account (Premium or higher recommended)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Stripe account (test mode works — connect via Vercel Marketplace)
- Course content prepared (lesson text, video URLs, quiz questions)
Build steps
Set up the database schema for courses, modules, and lessons
Create the Supabase schema for the course structure with modules, lessons (video/text/quiz types), enrollments, progress tracking, and certificates.
1// Paste this prompt into V0's AI chat:2// Build an online education platform. Create a Supabase schema:3// 1. courses: id (uuid PK), title (text), slug (text UNIQUE), description (text), instructor_id (uuid FK to auth.users), price_cents (int), stripe_price_id (text), thumbnail_url (text), status (text DEFAULT 'draft'), published_at (timestamptz)4// 2. modules: id (uuid PK), course_id (uuid FK to courses), title (text), position (int)5// 3. lessons: id (uuid PK), module_id (uuid FK to modules), title (text), type (text CHECK IN 'video','text','quiz'), content (jsonb), video_url (text), duration_minutes (int), position (int)6// 4. enrollments: id (uuid PK), user_id (uuid FK to auth.users), course_id (uuid FK to courses), stripe_subscription_id (text), enrolled_at (timestamptz), completed_at (timestamptz)7// 5. lesson_progress: user_id (uuid FK to auth.users), lesson_id (uuid FK to lessons), status (text DEFAULT 'not_started'), score (int), completed_at (timestamptz), PRIMARY KEY (user_id, lesson_id)8// 6. certificates: id (uuid PK), enrollment_id (uuid FK to enrollments), issued_at (timestamptz), certificate_url (text)9// Add RLS. Generate SQL and types.Pro tip: Use V0's Stripe integration via Vercel Marketplace for course purchases and the Connect panel for Supabase setup in a single workflow.
Expected result: All tables created with proper foreign keys and RLS policies for student-scoped access.
Build the course catalog and landing pages
Create the public course pages with SEO-friendly Server Components showing course details, syllabus, and purchase buttons.
1// Paste this prompt into V0's AI chat:2// Create education platform pages:3// 1. app/courses/page.tsx — course catalog with Card grid: thumbnail, title, instructor name, price, lesson count, rating. Add category Tabs and search Input.4// 2. app/courses/[slug]/page.tsx — course landing page: hero with thumbnail, title, instructor Avatar, price, 'Enroll Now' Button. Syllabus section with Accordion showing modules and lessons. 'Enroll Now' creates a Stripe Checkout session via Server Action.5// Use ISR with revalidate = 3600 for fast cached pages. Server Components for SEO.Expected result: The catalog shows courses with ISR caching. Landing pages have a rich syllabus Accordion and Stripe Checkout enrollment.
Build the lesson viewer with progress tracking
Create the learning interface where enrolled students view lessons, take quizzes, and track their progress through the course.
1// Paste this prompt into V0's AI chat:2// Create the lesson viewer at app/learn/[courseId]/[lessonId]/page.tsx.3// Requirements:4// - Left sidebar: module/lesson tree with Accordion. Show completion status per lesson (checkmark icon, in-progress icon, or locked icon). Current lesson highlighted.5// - Main area: lesson content based on type:6// - video: embedded video player with video_url7// - text: rendered markdown/rich text content from jsonb8// - quiz: RadioGroup for multiple choice, submit Button, score display9// - 'Mark as Complete' Button for video/text lessons10// - Quiz auto-completes on submit with score tracking11// - Progress bar at top showing course completion percentage12// - Navigation Buttons: Previous Lesson, Next Lesson13// - Server Action for marking lessons complete and submitting quiz answers14// - On last lesson complete, check if all lessons done and show celebration + certificate generation linkExpected result: The lesson viewer shows content with sidebar navigation, progress tracking, and quiz submission with scoring.
Set up Stripe webhook and certificate generation
Create the webhook handler for enrollment creation on purchase and the certificate generation endpoint that runs when a course is completed.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)78export async function POST(req: NextRequest) {9 const rawBody = await req.text()10 const sig = req.headers.get('stripe-signature')!1112 let event: Stripe.Event13 try {14 event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!)15 } catch {16 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })17 }1819 if (event.type === 'checkout.session.completed') {20 const session = event.data.object as Stripe.Checkout.Session21 const { user_id, course_id } = session.metadata || {}2223 if (user_id && course_id) {24 await supabase.from('enrollments').insert({25 user_id,26 course_id,27 enrolled_at: new Date().toISOString(),28 })29 }30 }3132 return NextResponse.json({ received: true })33}Expected result: Successful course purchases automatically create enrollment records. Students can immediately access course content.
Add quiz scoring and deploy
Build the quiz submission logic that scores answers server-side and the completion check that triggers certificate generation. Then deploy.
1// Paste this prompt into V0's AI chat:2// Add quiz and completion features:3// 1. Server Action submitQuiz: receives attempt answers, fetches correct answers from lessons.content jsonb, computes score server-side (NEVER client-side to prevent cheating), stores in lesson_progress with score4// 2. Server Action completeLesson: marks lesson as completed, checks if ALL lessons in course are completed. If yes, triggers certificate generation.5// 3. Certificate generation: POST to /api/certificates/generate with enrollment_id. Generate a simple PDF with course title, student name, completion date, and a certificate ID. Upload to Supabase Storage and store URL in certificates table.6// 4. Show certificate download Button on completed courses in the student dashboard7// 5. Student dashboard at app/dashboard/page.tsx showing enrolled courses with Progress bars and certificate linksPro tip: Use ISR with revalidate = 3600 for course landing pages so they load from the CDN edge but update within an hour when content changes.
Expected result: Quizzes score server-side, completion triggers certificates, and the student dashboard shows progress. The app is deployed.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const rawBody = await req.text()13 const sig = req.headers.get('stripe-signature')!1415 let event: Stripe.Event16 try {17 event = stripe.webhooks.constructEvent(18 rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!19 )20 } catch {21 return NextResponse.json({ error: 'Invalid sig' }, { status: 400 })22 }2324 if (event.type === 'checkout.session.completed') {25 const session = event.data.object as Stripe.Checkout.Session26 const meta = session.metadata || {}2728 if (meta.user_id && meta.course_id) {29 const { error } = await supabase.from('enrollments').insert({30 user_id: meta.user_id,31 course_id: meta.course_id,32 enrolled_at: new Date().toISOString(),33 })3435 if (error) console.error('Enrollment insert failed:', error)36 }37 }3839 return NextResponse.json({ received: true })40}Customization ideas
Discussion forums per lesson
Add a comments section below each lesson where students ask questions and instructors reply.
Drip content release
Unlock modules on a weekly schedule after enrollment date instead of releasing all content at once.
Instructor analytics
Build a dashboard showing enrollment trends, lesson completion rates, quiz performance, and revenue per course.
Subscription-based access
Replace one-time purchases with monthly subscriptions using Stripe Billing for all-access passes.
Common pitfalls
Pitfall: Scoring quizzes on the client side
How to avoid: Score quizzes in a Server Action. The client sends answer IDs; the server fetches correct answers from the database, computes the score, and stores results.
Pitfall: Using request.json() for Stripe webhook body
How to avoid: Use request.text() and pass the raw body to stripe.webhooks.constructEvent().
Pitfall: Not checking enrollment before showing lesson content
How to avoid: Check the enrollments table in the lesson viewer Server Component. If no active enrollment exists, redirect to the course landing page with a purchase prompt.
Best practices
- Score quizzes server-side in Server Actions — never expose correct answers to the client
- Use ISR with revalidate = 3600 for course landing pages for fast CDN-cached loads
- Check enrollment status in the lesson viewer Server Component before rendering any content
- Use Stripe Checkout metadata to pass user_id and course_id through the payment flow
- Always use request.text() for Stripe webhook signature verification
- Use V0's Design Mode (Option+D) to adjust course Card layouts and lesson viewer spacing without spending credits
- Store quiz questions and answers in the lessons.content JSONB field for flexible content types
- Generate certificates server-side to avoid shipping heavy PDF libraries to the client
AI prompts to try
Copy these prompts to build this project faster.
I'm building an online education platform with Next.js App Router, Supabase, and Stripe. When a student completes all lessons in a course, I need to generate a PDF certificate with the course title, student name, and completion date. Please write the API route at app/api/certificates/generate/route.ts that generates a simple certificate, uploads it to Supabase Storage, and stores the URL in the certificates table.
Create a quiz component that renders questions from a JSONB array. Each question has text, type (multiple_choice/true_false), options array, and correct_answer. Show questions one at a time with RadioGroup for answers, Progress bar for question progress, and Previous/Next/Submit Buttons. On submit, POST answers to a Server Action that scores them server-side and returns the results. Show a score Card with correct/incorrect breakdown.
Frequently asked questions
How does course enrollment work?
Students click 'Enroll Now' which creates a Stripe Checkout session. After payment, the webhook creates an enrollment record linking the user to the course. The student can immediately access all lessons.
Can I add video lessons?
Yes. Lessons with type 'video' display an embedded video player. Store video URLs from YouTube, Vimeo, or any host in the video_url field. The lesson viewer renders the appropriate player.
How are quizzes scored?
Students submit their answers via a Server Action. The server fetches correct answers from the database, computes the score, and stores results. Correct answers are never sent to the browser to prevent cheating.
Do I need a paid V0 plan?
Premium ($20/month) is recommended. The education platform has multiple complex pages that require several prompts to build.
How are certificates generated?
When all lessons are completed, a Server Action triggers the certificate API which generates a PDF server-side, uploads it to Supabase Storage, and stores the URL. Students download from their dashboard.
How do I deploy the platform?
Click Share in V0, then Publish to Production. Set Stripe and Supabase keys in the Vars tab. Register the webhook URL in Stripe Dashboard for checkout.session.completed.
Can RapidDev help build a custom education platform?
Yes. RapidDev has built over 600 apps including learning management systems with video hosting, live classes, and analytics. Book a free consultation to discuss your platform.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation