# How to Build Online education platform with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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)

## Step-by-step guide

### 1. 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.

```
// Paste this prompt into V0's AI chat:
// Build an online education platform. Create a Supabase schema:
// 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)
// 2. modules: id (uuid PK), course_id (uuid FK to courses), title (text), position (int)
// 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)
// 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)
// 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)
// 6. certificates: id (uuid PK), enrollment_id (uuid FK to enrollments), issued_at (timestamptz), certificate_url (text)
// 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.

### 2. Build the course catalog and landing pages

Create the public course pages with SEO-friendly Server Components showing course details, syllabus, and purchase buttons.

```
// Paste this prompt into V0's AI chat:
// Create education platform pages:
// 1. app/courses/page.tsx — course catalog with Card grid: thumbnail, title, instructor name, price, lesson count, rating. Add category Tabs and search Input.
// 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.
// 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.

### 3. 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.

```
// Paste this prompt into V0's AI chat:
// Create the lesson viewer at app/learn/[courseId]/[lessonId]/page.tsx.
// Requirements:
// - Left sidebar: module/lesson tree with Accordion. Show completion status per lesson (checkmark icon, in-progress icon, or locked icon). Current lesson highlighted.
// - Main area: lesson content based on type:
//   - video: embedded video player with video_url
//   - text: rendered markdown/rich text content from jsonb
//   - quiz: RadioGroup for multiple choice, submit Button, score display
// - 'Mark as Complete' Button for video/text lessons
// - Quiz auto-completes on submit with score tracking
// - Progress bar at top showing course completion percentage
// - Navigation Buttons: Previous Lesson, Next Lesson
// - Server Action for marking lessons complete and submitting quiz answers
// - On last lesson complete, check if all lessons done and show celebration + certificate generation link
```

**Expected result:** The lesson viewer shows content with sidebar navigation, progress tracking, and quiz submission with scoring.

### 4. 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.

```
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)

export async function POST(req: NextRequest) {
  const rawBody = await req.text()
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session
    const { user_id, course_id } = session.metadata || {}

    if (user_id && course_id) {
      await supabase.from('enrollments').insert({
        user_id,
        course_id,
        enrolled_at: new Date().toISOString(),
      })
    }
  }

  return NextResponse.json({ received: true })
}
```

**Expected result:** Successful course purchases automatically create enrollment records. Students can immediately access course content.

### 5. 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.

```
// Paste this prompt into V0's AI chat:
// Add quiz and completion features:
// 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 score
// 2. Server Action completeLesson: marks lesson as completed, checks if ALL lessons in course are completed. If yes, triggers certificate generation.
// 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.
// 4. Show certificate download Button on completed courses in the student dashboard
// 5. Student dashboard at app/dashboard/page.tsx showing enrolled courses with Progress bars and certificate links
```

> Pro 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 example

File: `app/api/webhooks/stripe/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const rawBody = await req.text()
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(
      rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch {
    return NextResponse.json({ error: 'Invalid sig' }, { status: 400 })
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session
    const meta = session.metadata || {}

    if (meta.user_id && meta.course_id) {
      const { error } = await supabase.from('enrollments').insert({
        user_id: meta.user_id,
        course_id: meta.course_id,
        enrolled_at: new Date().toISOString(),
      })

      if (error) console.error('Enrollment insert failed:', error)
    }
  }

  return NextResponse.json({ received: true })
}
```

## Common mistakes

- **Scoring quizzes on the client side** — Students can inspect JavaScript to see correct answers before submitting, defeating the purpose of assessment. Fix: 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.
- **Using request.json() for Stripe webhook body** — Stripe signature verification needs the raw body. Parsing breaks the format and verification fails. Fix: Use request.text() and pass the raw body to stripe.webhooks.constructEvent().
- **Not checking enrollment before showing lesson content** — Without enrollment verification, anyone with the URL can access paid course content. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/online-education-platform
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/online-education-platform
