Video Course Platform Implementation Plan
Project: Coursera-like Video Tutorial Platform Technology: YouTube Unlisted Videos + Supabase + React Created: 2025-10-30 Status: Planning Phase
Table of Contents
- Executive Summary
- Database Schema Design
- Admin Panel Architecture
- Student Dashboard Architecture
- YouTube Integration Strategy
- Implementation Roadmap
- Key API Endpoints
- Security Considerations
- Cost Analysis
- Example Data Models
- Decision Points
- Progress Tracker
Executive Summary
Building a Coursera-like video tutorial platform using YouTube unlisted videos with existing Supabase infrastructure. The platform will have an Admin Panel for course management and a Student Dashboard for learning progress tracking.
Why YouTube Unlisted?
Pros:
- ✅ Free hosting & bandwidth
- ✅ Free transcoding (all quality variants)
- ✅ Global CDN delivery
- ✅ Adaptive streaming
- ✅ Mobile optimization
- ✅ Subtitles/Captions support
- ✅ Speed controls (0.5x to 2x)
- ✅ 99.9% uptime
Cost Savings: $200-500/month vs self-hosted solution
Cons:
- ⚠️ YouTube branding
- ⚠️ Limited player customization
- ⚠️ Potential takedowns (rare)
- ⚠️ URL can be shared
1. Database Schema Design
Current State Analysis
Existing tables:
nova_tracks(courses)nova_modules(course content - text only)course_enrollmentscourse_progresscourse_resources
New Tables Needed
1.1 Extend Existing nova_tracks Table
-- Add columns for video course features
ALTER TABLE nova_tracks ADD COLUMN IF NOT EXISTS
instructor_name TEXT,
thumbnail_url TEXT,
trailer_youtube_id TEXT,
total_lessons INTEGER DEFAULT 0,
estimated_hours DECIMAL(4,1);
1.2 Course Sections Table
-- COURSE SECTIONS (Modules/Chapters)
CREATE TABLE course_sections (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
course_id UUID NOT NULL REFERENCES nova_tracks(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(course_id, position)
);
CREATE INDEX idx_sections_course ON course_sections(course_id);
1.3 Course Lessons Table (VIDEO CONTENT)
-- LESSONS (Video Content)
CREATE TABLE course_lessons (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
section_id UUID NOT NULL REFERENCES course_sections(id) ON DELETE CASCADE,
course_id UUID NOT NULL REFERENCES nova_tracks(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
youtube_video_id TEXT NOT NULL, -- "dQw4w9WgXcQ" from youtube.com/watch?v=dQw4w9WgXcQ
duration_seconds INTEGER, -- Auto-extracted from YouTube API
position INTEGER NOT NULL DEFAULT 0,
is_preview BOOLEAN DEFAULT FALSE, -- Allow non-enrolled users to watch
resources_text TEXT, -- Additional notes/resources
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(section_id, position)
);
CREATE INDEX idx_lessons_course ON course_lessons(course_id);
CREATE INDEX idx_lessons_section ON course_lessons(section_id);
1.4 Lesson Progress Table
-- LESSON PROGRESS (Track video completion)
CREATE TABLE lesson_progress (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
lesson_id UUID NOT NULL REFERENCES course_lessons(id) ON DELETE CASCADE,
enrollment_id UUID NOT NULL REFERENCES course_enrollments(id) ON DELETE CASCADE,
watch_time_seconds INTEGER DEFAULT 0,
completed BOOLEAN DEFAULT FALSE,
last_watched_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, lesson_id)
);
CREATE INDEX idx_progress_user ON lesson_progress(user_id);
CREATE INDEX idx_progress_lesson ON lesson_progress(lesson_id);
CREATE INDEX idx_progress_enrollment ON lesson_progress(enrollment_id);
1.5 Lesson Notes Table
-- LESSON NOTES (Student's personal notes per lesson)
CREATE TABLE lesson_notes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
lesson_id UUID NOT NULL REFERENCES course_lessons(id) ON DELETE CASCADE,
note_text TEXT NOT NULL,
timestamp_seconds INTEGER, -- Optional: note at specific video timestamp
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_notes_user_lesson ON lesson_notes(user_id, lesson_id);
1.6 Quiz Questions Table (Optional)
-- QUIZ QUESTIONS (Optional for assessments)
CREATE TABLE quiz_questions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
lesson_id UUID REFERENCES course_lessons(id) ON DELETE CASCADE,
section_id UUID REFERENCES course_sections(id) ON DELETE CASCADE,
question_text TEXT NOT NULL,
question_type TEXT CHECK (question_type IN ('multiple_choice', 'true_false', 'short_answer')),
options JSONB, -- {choices: ["A", "B", "C"], correct: 0}
position INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
1.7 Admin Users Table
-- ADMIN USERS (Role-based access)
CREATE TABLE admin_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT CHECK (role IN ('super_admin', 'course_admin', 'instructor')),
permissions JSONB DEFAULT '{"can_create_courses": true, "can_edit_all": false}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id)
);
CREATE INDEX idx_admin_user ON admin_users(user_id);
1.8 Course Certificates Table
-- COURSE CERTIFICATES
CREATE TABLE course_certificates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
course_id UUID NOT NULL REFERENCES nova_tracks(id) ON DELETE CASCADE,
certificate_code TEXT UNIQUE NOT NULL,
issued_at TIMESTAMPTZ DEFAULT NOW(),
completion_percentage DECIMAL(5,2) DEFAULT 100.00,
UNIQUE(user_id, course_id)
);
CREATE INDEX idx_certificates_user ON course_certificates(user_id);
CREATE INDEX idx_certificates_code ON course_certificates(certificate_code);
1.9 Additional Indexes
-- Optimize enrollment queries
CREATE INDEX idx_enrollments_user ON course_enrollments(user_id);
CREATE INDEX idx_enrollments_course ON course_enrollments(course_id);
CREATE INDEX idx_enrollments_status ON course_enrollments(status);
Database Migration File
File to create: supabase/migrations/20251030000000_video_courses.sql
2. Admin Panel Architecture
2.1 Routes Structure
/admin
/dashboard - Overview stats (total courses, students, revenue)
/courses - List all courses (grid/table view)
/courses/new - Create new course form
/courses/:id/edit - Edit course details
/courses/:id/sections - Manage sections (add/edit/delete/reorder)
/courses/:id/lessons - Manage lessons (add YouTube videos)
/students - View enrolled students
/students/:id - Individual student progress
/analytics - Course performance metrics
/settings - Admin settings
2.2 Admin Panel Features
A. Course Management
1. CREATE COURSE
- Basic info (title, description, instructor)
- Upload thumbnail (or use YouTube thumbnail)
- Set pricing tier (free/paid)
- Add YouTube trailer (unlisted)
- Set course status (draft/published)
2. MANAGE SECTIONS
- Add/Edit/Delete sections
- Drag-and-drop reordering
- Collapse/Expand UI for better organization
- Bulk operations
3. ADD LESSONS (VIDEO UPLOAD WORKFLOW)
Admin Input: YouTube unlisted URL
↓
Parse video ID from URL
↓
Call YouTube Data API to fetch:
- Video title
- Duration
- Thumbnail
- Description
↓
Display in form for admin to edit metadata
↓
Set lesson position (auto or manual)
↓
Mark as "preview" (free) or locked
↓
Save to database
4. BULK OPERATIONS
- Import lessons from CSV
- Bulk delete lessons
- Bulk reorder
- Bulk update settings (all lessons in section)
5. PREVIEW MODE
- View course as student would see it
- Test video playback
- Check lesson locking logic
B. Student Management
Features:
- View all enrolled students
- Filter by course/status
- Export enrollment data (CSV)
- Manual enrollment/unenrollment
- Send course announcement emails
- View individual student progress
C. Analytics Dashboard
Metrics to Display:
- Total enrollments (all time, this month)
- Course completion rates
- Average watch time per course
- Most popular lessons
- Drop-off points (where students quit)
- Daily/weekly active users
- Revenue (if paid courses)
2.3 Admin UI Component Hierarchy
<AdminLayout>
<AdminSidebar>
- Dashboard
- Courses
- Students
- Analytics
- Settings
</AdminSidebar>
<AdminHeader>
- Breadcrumbs
- User menu
- Notifications
</AdminHeader>
<AdminContent>
Routes:
- <AdminDashboard />
- <CourseList />
- <CourseEditor />
- <SectionManager />
- <LessonManager />
- <StudentTable />
- <AnalyticsDashboard />
</AdminContent>
</AdminLayout>
2.4 Key Admin Components
Components to create:
components/admin/
├── AdminLayout.jsx
├── AdminSidebar.jsx
├── AdminHeader.jsx
├── CourseList.jsx
├── CourseEditor.jsx
├── SectionManager.jsx
├── LessonManager.jsx
├── YouTubeVideoInput.jsx ← Key component for video upload
├── LessonForm.jsx
├── DragDropReorder.jsx
├── StudentTable.jsx
├── StudentProgress.jsx
├── AnalyticsDashboard.jsx
├── AnalyticsChart.jsx
└── BulkActions.jsx
pages/admin/
├── Dashboard.jsx
├── CourseList.jsx
├── CourseEditor.jsx
├── Students.jsx
└── Analytics.jsx
src/services/
├── adminCourses.js
├── adminStudents.js
└── adminAnalytics.js
3. Student Dashboard Architecture
3.1 Routes Structure
/dashboard
/ - My courses overview
/courses/:slug - Course detail page (curriculum preview)
/courses/:slug/learn - Video player + lesson list (main learning page)
/courses/:slug/learn/:lessonId - Specific lesson
/progress - Overall progress tracking
/certificates - Earned certificates
/settings - Profile settings
3.2 Dashboard Features
A. My Courses View
Display:
- Enrolled courses (grid or list view)
- Progress bars (% complete per course)
- "Continue watching" button (resumes last lesson)
- Recently added courses
- Recommended courses based on completed courses
- Filter by: All / In Progress / Completed
Data Structure Per Course:
{
course: {
id: "uuid",
title: "Course Name",
slug: "course-name",
thumbnail_url: "...",
instructor_name: "...",
total_lessons: 24,
estimated_hours: 8.5
},
enrollment: {
enrolled_at: "2025-10-01",
status: "active"
},
progress: {
completed_lessons: 5,
total_lessons: 24,
percentage: 20.8,
last_watched_lesson: {
id: "uuid",
title: "Lesson 5",
section_title: "Module 2"
},
last_watched_at: "2025-10-29"
}
}
B. Course Learning Page (Main Video Player)
Layout:
┌─────────────────────────────────────────────────────────┐
│ Course Header │
│ Course Title | Progress: 5/24 (20%) | Certificate │
├──────────────┬──────────────────────────────────────────┤
│ │ │
│ LESSON │ VIDEO PLAYER │
│ SIDEBAR │ (YouTube Embed - 16:9 responsive) │
│ │ │
│ □ Intro │ ───────────────────────────────── │
│ ✓ 1.1 │ Current Lesson: "Introduction to AI" │
│ ⏸ 1.2 │ Duration: 12:30 │
│ ○ 1.3 │ │
│ 🔒 1.4 │ ───────────────────────────────── │
│ │ │
│ □ Module 2 │ 📄 Resources: │
│ ○ 2.1 │ - Download slides.pdf │
│ ○ 2.2 │ - Code samples (GitHub) │
│ │ │
│ │ 📝 My Notes: │
│ │ [Add a note at 5:23...] │
│ │ │
│ │ - Note at 2:15: "Key concept here" │
│ │ - Note at 8:40: "Practice this" │
│ │ │
└──────────────┴──────────────────────────────────────────┘
Features:
- ✓ = Completed (green checkmark)
- ⏸ = In Progress (blue)
- 🔒 = Locked (if sequential learning enabled)
- ○ = Not started (gray)
- Auto-mark complete at 90% watch time
- Continue where you left off (resume playback)
- Auto-advance to next lesson option
- Keyboard shortcuts (Space = play/pause, Arrow keys = seek)
- Fullscreen mode
- Playback speed control (inherited from YouTube)
C. Progress Tracking Page
Display:
- Overall completion stats (pie chart)
- Time spent learning (total hours)
- Certificates earned (badge gallery)
- Streak tracking (days active)
- Milestones achieved:
- 🎯 First lesson completed
- 🔥 10 lessons completed
- 💪 50 lessons completed
- 🏆 First course completed
- 🌟 5 courses completed
SQL Query for Stats:
SELECT
COUNT(DISTINCT ce.course_id) as courses_enrolled,
COUNT(DISTINCT CASE WHEN lp.completed = TRUE THEN lp.lesson_id END) as lessons_completed,
COUNT(DISTINCT CASE WHEN cc.id IS NOT NULL THEN cc.course_id END) as courses_completed,
SUM(lp.watch_time_seconds) as total_watch_time_seconds
FROM course_enrollments ce
LEFT JOIN lesson_progress lp ON lp.user_id = ce.user_id
LEFT JOIN course_certificates cc ON cc.user_id = ce.user_id AND cc.course_id = ce.course_id
WHERE ce.user_id = $1
3.3 Student UI Components
Components to create:
components/dashboard/
├── DashboardLayout.jsx
├── DashboardSidebar.jsx
├── CourseCard.jsx
├── CourseGrid.jsx
├── ProgressBar.jsx
├── ProgressCircle.jsx
├── LessonSidebar.jsx
├── LessonItem.jsx
├── VideoPlayer.jsx ← Key component
├── VideoControls.jsx
├── LessonNotes.jsx
├── NoteForm.jsx
├── ResourcesList.jsx
├── CertificateCard.jsx
└── MilestonesBadge.jsx
pages/dashboard/
├── MyCourses.jsx
├── CourseDetail.jsx
├── LearningPage.jsx ← Main video learning page
├── Progress.jsx
├── Certificates.jsx
└── Settings.jsx
src/services/
├── studentCourses.js
├── lessonProgress.js
├── certificates.js
└── notes.js
4. YouTube Integration Strategy
4.1 YouTube Data API Setup
Get API Key:
- Go to Google Cloud Console
- Create new project
- Enable "YouTube Data API v3"
- Create credentials (API Key)
- Add to
.env:VITE_YOUTUBE_API_KEY=your_key_here
API Quota: 10,000 units/day (each video metadata request = 1 unit)
4.2 YouTube Service
File: src/services/youtube.js
const YOUTUBE_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY;
const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';
/**
* Extract video ID from YouTube URL
* Supports:
* - https://www.youtube.com/watch?v=VIDEO_ID
* - https://youtu.be/VIDEO_ID
* - https://www.youtube.com/embed/VIDEO_ID
*/
export function extractVideoId(url) {
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
/^([a-zA-Z0-9_-]{11})$/ // Already just an ID
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}
/**
* Validate YouTube video ID format
*/
export function validateYouTubeId(id) {
return /^[a-zA-Z0-9_-]{11}$/.test(id);
}
/**
* Fetch video metadata from YouTube Data API
*/
export async function getVideoMetadata(videoId) {
if (!validateYouTubeId(videoId)) {
throw new Error('Invalid YouTube video ID format');
}
const url = `${YOUTUBE_API_BASE}/videos?id=${videoId}&part=snippet,contentDetails&key=${YOUTUBE_API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
throw new Error(`YouTube API Error: ${data.error.message}`);
}
if (!data.items || data.items.length === 0) {
throw new Error('Video not found or is private');
}
const video = data.items[0];
return {
id: videoId,
title: video.snippet.title,
description: video.snippet.description,
thumbnail: video.snippet.thumbnails.high?.url || video.snippet.thumbnails.default.url,
duration_seconds: parseDuration(video.contentDetails.duration),
channel: video.snippet.channelTitle,
published_at: video.snippet.publishedAt,
};
}
/**
* Convert ISO 8601 duration to seconds
* Example: PT1H2M10S -> 3730 seconds
*/
function parseDuration(isoDuration) {
const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!match) return 0;
const hours = parseInt(match[1] || 0);
const minutes = parseInt(match[2] || 0);
const seconds = parseInt(match[3] || 0);
return hours * 3600 + minutes * 60 + seconds;
}
/**
* Format seconds to HH:MM:SS or MM:SS
*/
export function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
/**
* Get YouTube embed URL
*/
export function getEmbedUrl(videoId, options = {}) {
const params = new URLSearchParams({
modestbranding: options.modestbranding ?? 1,
rel: options.rel ?? 0,
origin: window.location.origin,
...options
});
return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
}
4.3 Video Player Component
File: components/VideoPlayer.jsx
import { useEffect, useRef, useState } from 'react';
/**
* YouTube IFrame API Video Player
* Docs: https://developers.google.com/youtube/iframe_api_reference
*/
export function VideoPlayer({
youtubeId,
onProgress,
onComplete,
startTime = 0,
autoplay = false
}) {
const playerRef = useRef(null);
const containerRef = useRef(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
// Load YouTube IFrame API
if (!window.YT) {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(tag);
}
// Initialize player when API is ready
window.onYouTubeIframeAPIReady = () => {
playerRef.current = new window.YT.Player('youtube-player', {
videoId: youtubeId,
playerVars: {
modestbranding: 1, // Minimize YouTube branding
rel: 0, // Don't show related videos at end
origin: window.location.origin,
start: startTime, // Resume from last position
autoplay: autoplay ? 1 : 0,
},
events: {
onReady: handlePlayerReady,
onStateChange: handleStateChange,
},
});
};
// If API already loaded
if (window.YT && window.YT.Player) {
window.onYouTubeIframeAPIReady();
}
return () => {
if (playerRef.current?.destroy) {
playerRef.current.destroy();
}
};
}, [youtubeId]);
const handlePlayerReady = (event) => {
setIsReady(true);
if (autoplay) {
event.target.playVideo();
}
};
const handleStateChange = (event) => {
// Video ended
if (event.data === window.YT.PlayerState.ENDED) {
onComplete?.();
}
};
// Track progress every 5 seconds
useEffect(() => {
if (!isReady) return;
const interval = setInterval(() => {
if (playerRef.current?.getCurrentTime && playerRef.current?.getDuration) {
const currentTime = Math.floor(playerRef.current.getCurrentTime());
const duration = Math.floor(playerRef.current.getDuration());
if (duration > 0) {
onProgress?.(currentTime, duration);
}
}
}, 5000); // Update every 5 seconds
return () => clearInterval(interval);
}, [isReady, onProgress]);
return (
<div
ref={containerRef}
className="video-player-container"
style={{
position: 'relative',
paddingBottom: '56.25%', // 16:9 aspect ratio
height: 0,
overflow: 'hidden'
}}
>
<div
id="youtube-player"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%'
}}
/>
</div>
);
}
4.4 Progress Tracking Logic
File: src/services/lessonProgress.js
import { supabase } from '../lib/supabaseClient';
const COMPLETION_THRESHOLD = 0.9; // 90% watched = complete
/**
* Update lesson progress for a user
*/
export async function updateLessonProgress(
userId,
lessonId,
enrollmentId,
watchTimeSeconds,
totalDuration
) {
const completionPercentage = watchTimeSeconds / totalDuration;
const isCompleted = completionPercentage >= COMPLETION_THRESHOLD;
const { data, error } = await supabase
.from('lesson_progress')
.upsert({
user_id: userId,
lesson_id: lessonId,
enrollment_id: enrollmentId,
watch_time_seconds: watchTimeSeconds,
completed: isCompleted,
last_watched_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}, {
onConflict: 'user_id,lesson_id'
})
.select()
.single();
if (error) throw error;
// If lesson just completed, check if course is complete
if (isCompleted && data) {
await checkCourseCompletion(userId, lessonId, enrollmentId);
}
return data;
}
/**
* Check if user completed entire course
*/
async function checkCourseCompletion(userId, lessonId, enrollmentId) {
// Get course ID from lesson
const { data: lesson } = await supabase
.from('course_lessons')
.select('course_id')
.eq('id', lessonId)
.single();
if (!lesson) return;
// Count completed vs total lessons
const { data: totalLessons } = await supabase
.from('course_lessons')
.select('id', { count: 'exact', head: true })
.eq('course_id', lesson.course_id);
const { data: completedLessons } = await supabase
.from('lesson_progress')
.select('id', { count: 'exact', head: true })
.eq('user_id', userId)
.eq('enrollment_id', enrollmentId)
.eq('completed', true);
const totalCount = totalLessons?.count || 0;
const completedCount = completedLessons?.count || 0;
// If 100% complete, issue certificate
if (totalCount > 0 && completedCount >= totalCount) {
await issueCertificate(userId, lesson.course_id);
}
}
/**
* Issue certificate for course completion
*/
async function issueCertificate(userId, courseId) {
// Check if certificate already exists
const { data: existing } = await supabase
.from('course_certificates')
.select('id')
.eq('user_id', userId)
.eq('course_id', courseId)
.single();
if (existing) return existing;
// Generate unique certificate code
const certificateCode = generateCertificateCode();
const { data, error } = await supabase
.from('course_certificates')
.insert({
user_id: userId,
course_id: courseId,
certificate_code: certificateCode,
completion_percentage: 100.00,
})
.select()
.single();
if (error) throw error;
// TODO: Send congratulations email
return data;
}
/**
* Generate unique certificate code
*/
function generateCertificateCode() {
const timestamp = Date.now().toString(36).toUpperCase();
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `CERT-${timestamp}-${random}`;
}
/**
* Get user's progress for a specific course
*/
export async function getCourseProgress(userId, courseId) {
const { data, error } = await supabase.rpc('get_course_progress', {
p_user_id: userId,
p_course_id: courseId
});
if (error) throw error;
return data;
}
Database Function (add to migration):
-- Function to get course progress statistics
CREATE OR REPLACE FUNCTION get_course_progress(
p_user_id UUID,
p_course_id UUID
)
RETURNS TABLE(
total_lessons INTEGER,
completed_lessons INTEGER,
in_progress_lessons INTEGER,
total_duration_seconds INTEGER,
watched_duration_seconds INTEGER,
completion_percentage DECIMAL(5,2),
last_watched_at TIMESTAMPTZ
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(cl.id)::INTEGER as total_lessons,
COUNT(lp.id) FILTER (WHERE lp.completed = TRUE)::INTEGER as completed_lessons,
COUNT(lp.id) FILTER (WHERE lp.completed = FALSE AND lp.watch_time_seconds > 0)::INTEGER as in_progress_lessons,
COALESCE(SUM(cl.duration_seconds), 0)::INTEGER as total_duration_seconds,
COALESCE(SUM(lp.watch_time_seconds), 0)::INTEGER as watched_duration_seconds,
CASE
WHEN COUNT(cl.id) > 0 THEN
(COUNT(lp.id) FILTER (WHERE lp.completed = TRUE)::DECIMAL / COUNT(cl.id) * 100)
ELSE 0
END as completion_percentage,
MAX(lp.last_watched_at) as last_watched_at
FROM course_lessons cl
LEFT JOIN lesson_progress lp ON lp.lesson_id = cl.id AND lp.user_id = p_user_id
WHERE cl.course_id = p_course_id;
END;
$$ LANGUAGE plpgsql;
5. Implementation Roadmap
Phase 1: Database Setup ✅ (Week 1)
Priority: HIGH Estimated Time: 3-5 days
Tasks:
- Create migration file
supabase/migrations/20251030000000_video_courses.sql - Add all new tables (course_sections, course_lessons, lesson_progress, etc.)
- Add indexes for performance
- Add Row Level Security policies
- Create database functions (get_course_progress, etc.)
- Test with sample data
- Verify RLS policies work correctly
Files to create:
supabase/migrations/20251030000000_video_courses.sql
Testing Checklist:
- Can insert sample course
- Can insert sections and lessons
- Can track progress
- RLS prevents unauthorized access
- Indexes improve query performance
Phase 2: YouTube Integration ✅ (Week 1)
Priority: HIGH Estimated Time: 2-3 days
Tasks:
- Get YouTube Data API key from Google Cloud Console
- Add API key to environment variables
- Create
src/services/youtube.js - Implement video ID extraction
- Implement metadata fetching
- Test with various YouTube URL formats
- Create VideoPlayer component
- Test video embedding and playback
Files to create:
src/services/youtube.jscomponents/VideoPlayer.jsxsrc/hooks/useYouTubePlayer.js(optional)
Testing Checklist:
- Can extract video ID from different URL formats
- Can fetch video metadata (title, duration, thumbnail)
- Video player loads and plays correctly
- Progress tracking works during playback
- Video completes and triggers onComplete callback
Phase 3: Admin Panel - Core ✅ (Week 2-3)
Priority: HIGH Estimated Time: 7-10 days
Tasks:
Week 2:
- Set up admin routes with authentication
- Create AdminLayout component
- Create admin role checking middleware
- Build Dashboard page (basic stats)
- Build CourseList page (view all courses)
- Build CourseEditor page (create/edit course basic info)
Week 3:
- Build SectionManager component (add/edit/delete sections)
- Build LessonManager component
- Build YouTubeVideoInput component (paste URL, fetch metadata)
- Implement drag-and-drop reordering for sections/lessons
- Add delete confirmations
- Test full course creation workflow
Files to create:
pages/admin/
├── Dashboard.jsx
├── CourseList.jsx
├── CourseEditor.jsx
└── CourseContent.jsx (sections + lessons)
components/admin/
├── AdminLayout.jsx
├── AdminSidebar.jsx
├── AdminHeader.jsx
├── CourseForm.jsx
├── SectionManager.jsx
├── SectionForm.jsx
├── LessonManager.jsx
├── LessonForm.jsx
├── YouTubeVideoInput.jsx
└── DragDropReorder.jsx
src/services/
├── adminCourses.js
├── adminSections.js
└── adminLessons.js
src/hooks/
└── useAdminAuth.js
Testing Checklist:
- Can create new course
- Can add sections to course
- Can paste YouTube URL and fetch metadata
- Can add lesson with YouTube video
- Can reorder sections and lessons
- Can edit course/section/lesson
- Can delete course/section/lesson
- Only admin users can access /admin routes
Phase 4: Student Dashboard ✅ (Week 3-4)
Priority: HIGH Estimated Time: 7-10 days
Tasks:
Week 3:
- Create dashboard routes
- Build DashboardLayout component
- Build MyCourses page (show enrolled courses)
- Build CourseCard component with progress bar
- Implement enrollment flow
Week 4:
- Build CourseDetail page (curriculum overview)
- Build LearningPage (video player + lesson sidebar)
- Build LessonSidebar component
- Implement progress tracking (save to DB)
- Build LessonNotes component
- Build Progress page (stats and milestones)
- Test complete learning flow (enroll → watch → complete)
Files to create:
pages/dashboard/
├── MyCourses.jsx
├── CourseDetail.jsx
├── LearningPage.jsx
├── Progress.jsx
└── Settings.jsx
components/dashboard/
├── DashboardLayout.jsx
├── DashboardSidebar.jsx
├── CourseCard.jsx
├── CourseGrid.jsx
├── ProgressBar.jsx
├── ProgressCircle.jsx
├── LessonSidebar.jsx
├── LessonItem.jsx
├── LessonNotes.jsx
├── NoteForm.jsx
└── ResourcesList.jsx
src/services/
├── studentCourses.js
├── lessonProgress.js
└── notes.js
src/hooks/
├── useCourseProgress.js
├── useLessonProgress.js
└── useLessonNotes.js
Testing Checklist:
- Can view enrolled courses
- Can see progress percentage per course
- Can click course and see curriculum
- Can start watching lesson
- Progress updates every 5 seconds
- Lesson marks complete at 90% watched
- Can resume from last position
- Can add notes to lessons
- Progress page shows correct stats
Phase 5: Analytics & Polish ✅ (Week 4-5)
Priority: MEDIUM Estimated Time: 5-7 days
Tasks:
- Build admin analytics dashboard
- Add charts (enrollments over time, completion rates)
- Build StudentTable component (view all students)
- Build individual student progress view
- Add completion certificates
- Design certificate template
- Add email notifications (welcome, completion, etc.)
- Implement course search/filter
- Add drag-and-drop for reordering
- Mobile responsive optimization
- Performance optimization (lazy loading, caching)
Files to create:
pages/admin/
├── Analytics.jsx
└── Students.jsx
components/admin/
├── AnalyticsDashboard.jsx
├── AnalyticsChart.jsx
├── StudentTable.jsx
└── StudentProgress.jsx
pages/dashboard/
└── Certificates.jsx
components/dashboard/
├── CertificateCard.jsx
└── Certificate.jsx (printable)
src/services/
├── analytics.js
├── certificates.js
└── email.js
Testing Checklist:
- Analytics show correct data
- Can view all enrolled students
- Can view individual student progress
- Certificate auto-issues on course completion
- Certificate is downloadable/printable
- Email notifications send correctly
- Search and filters work
- Mobile responsive on all pages
Phase 6: Advanced Features ✅ (Week 6+)
Priority: LOW (Nice to have) Estimated Time: 10-15 days
Tasks:
- Quiz system (create quiz questions, take quizzes)
- Discussion forums per lesson
- Downloadable resources (PDFs, code files)
- Course reviews and ratings
- Student-to-student messaging
- Live session integration (Zoom/Google Meet)
- Assignments and submissions
- Instructor feedback system
- Gamification (badges, leaderboards)
- Course prerequisites
- Drip content (unlock lessons over time)
Files to create:
components/quiz/
├── QuizQuestion.jsx
├── QuizResults.jsx
└── QuizCreator.jsx
components/discussion/
├── DiscussionThread.jsx
├── CommentForm.jsx
└── CommentList.jsx
components/resources/
└── DownloadableResources.jsx
components/reviews/
├── ReviewForm.jsx
└── ReviewList.jsx
6. Key API Endpoints Needed
6.1 Admin APIs (Service Role Key Required)
Create Course with Content
// src/services/adminCourses.js
export async function createCourseWithContent(courseData) {
const { data: course, error: courseError } = await supabase
.from('nova_tracks')
.insert({
slug: courseData.slug,
code: courseData.code,
title: courseData.title,
description: courseData.description,
instructor_name: courseData.instructor_name,
thumbnail_url: courseData.thumbnail_url,
trailer_youtube_id: courseData.trailer_youtube_id,
pricing_tier: courseData.pricing_tier || 'free',
status: courseData.status || 'open',
})
.select()
.single();
if (courseError) throw courseError;
return course;
}
export async function updateCourse(courseId, updates) {
const { data, error } = await supabase
.from('nova_tracks')
.update(updates)
.eq('id', courseId)
.select()
.single();
if (error) throw error;
return data;
}
export async function deleteCourse(courseId) {
const { error } = await supabase
.from('nova_tracks')
.delete()
.eq('id', courseId);
if (error) throw error;
}
Manage Sections
// src/services/adminSections.js
export async function createSection(sectionData) {
const { data, error } = await supabase
.from('course_sections')
.insert({
course_id: sectionData.course_id,
title: sectionData.title,
description: sectionData.description,
position: sectionData.position,
})
.select()
.single();
if (error) throw error;
return data;
}
export async function updateSection(sectionId, updates) {
const { data, error } = await supabase
.from('course_sections')
.update(updates)
.eq('id', sectionId)
.select()
.single();
if (error) throw error;
return data;
}
export async function deleteSection(sectionId) {
const { error } = await supabase
.from('course_sections')
.delete()
.eq('id', sectionId);
if (error) throw error;
}
export async function reorderSections(courseId, sectionIds) {
// Update position for each section
const updates = sectionIds.map((sectionId, index) => ({
id: sectionId,
position: index,
}));
for (const update of updates) {
await supabase
.from('course_sections')
.update({ position: update.position })
.eq('id', update.id);
}
}
Manage Lessons
// src/services/adminLessons.js
import { getVideoMetadata } from './youtube';
export async function createLessonFromYouTubeUrl(url, lessonData) {
// Extract video ID and fetch metadata
const videoId = extractVideoId(url);
if (!videoId) throw new Error('Invalid YouTube URL');
const metadata = await getVideoMetadata(videoId);
// Insert lesson
const { data, error } = await supabase
.from('course_lessons')
.insert({
section_id: lessonData.section_id,
course_id: lessonData.course_id,
title: lessonData.title || metadata.title,
description: lessonData.description || metadata.description,
youtube_video_id: videoId,
duration_seconds: metadata.duration_seconds,
position: lessonData.position,
is_preview: lessonData.is_preview || false,
})
.select()
.single();
if (error) throw error;
// Update course total_lessons count
await updateCourseLessonCount(lessonData.course_id);
return data;
}
export async function updateLesson(lessonId, updates) {
const { data, error } = await supabase
.from('course_lessons')
.update(updates)
.eq('id', lessonId)
.select()
.single();
if (error) throw error;
return data;
}
export async function deleteLesson(lessonId) {
const { data: lesson } = await supabase
.from('course_lessons')
.select('course_id')
.eq('id', lessonId)
.single();
const { error } = await supabase
.from('course_lessons')
.delete()
.eq('id', lessonId);
if (error) throw error;
// Update course total_lessons count
if (lesson) {
await updateCourseLessonCount(lesson.course_id);
}
}
async function updateCourseLessonCount(courseId) {
const { count } = await supabase
.from('course_lessons')
.select('id', { count: 'exact', head: true })
.eq('course_id', courseId);
await supabase
.from('nova_tracks')
.update({ total_lessons: count })
.eq('id', courseId);
}
Admin Analytics
// src/services/adminAnalytics.js
export async function getAdminStats() {
// Total courses
const { count: totalCourses } = await supabase
.from('nova_tracks')
.select('id', { count: 'exact', head: true });
// Total enrollments
const { count: totalEnrollments } = await supabase
.from('course_enrollments')
.select('id', { count: 'exact', head: true });
// Total certificates issued
const { count: totalCertificates } = await supabase
.from('course_certificates')
.select('id', { count: 'exact', head: true });
// Total students (unique users enrolled)
const { data: uniqueStudents } = await supabase
.from('course_enrollments')
.select('user_id');
return {
totalCourses,
totalEnrollments,
totalCertificates,
totalStudents: new Set(uniqueStudents?.map(e => e.user_id)).size,
};
}
export async function getCourseAnalytics(courseId) {
const { data, error } = await supabase.rpc('get_course_analytics', {
p_course_id: courseId
});
if (error) throw error;
return data;
}
Add to migration:
-- Analytics function
CREATE OR REPLACE FUNCTION get_course_analytics(p_course_id UUID)
RETURNS TABLE(
total_enrollments BIGINT,
active_enrollments BIGINT,
completed_enrollments BIGINT,
avg_completion_percentage DECIMAL(5,2),
total_watch_time_hours DECIMAL(10,2)
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(ce.id) as total_enrollments,
COUNT(ce.id) FILTER (WHERE ce.status = 'active') as active_enrollments,
COUNT(cc.id) as completed_enrollments,
AVG(
(SELECT COUNT(*) FILTER (WHERE lp.completed = TRUE)::DECIMAL
FROM lesson_progress lp
WHERE lp.enrollment_id = ce.id)
/ NULLIF((SELECT COUNT(*) FROM course_lessons WHERE course_id = p_course_id), 0)
* 100
) as avg_completion_percentage,
COALESCE(SUM(
(SELECT SUM(lp.watch_time_seconds)
FROM lesson_progress lp
WHERE lp.enrollment_id = ce.id)
) / 3600.0, 0) as total_watch_time_hours
FROM course_enrollments ce
LEFT JOIN course_certificates cc ON cc.user_id = ce.user_id AND cc.course_id = ce.course_id
WHERE ce.course_id = p_course_id;
END;
$$ LANGUAGE plpgsql;
6.2 Student APIs (RLS Protected)
Get Enrolled Courses with Progress
// src/services/studentCourses.js
export async function getMyCoursesWithProgress(userId) {
const { data, error } = await supabase
.from('course_enrollments')
.select(`
id,
enrolled_at: created_at,
status,
course:nova_tracks (
id,
slug,
title,
description,
instructor_name,
thumbnail_url,
total_lessons,
estimated_hours
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
// Fetch progress for each course
const coursesWithProgress = await Promise.all(
data.map(async (enrollment) => {
const progress = await getCourseProgress(userId, enrollment.course.id);
// Get last watched lesson
const { data: lastLesson } = await supabase
.from('lesson_progress')
.select(`
lesson:course_lessons (
id,
title,
section:course_sections (title)
),
last_watched_at
`)
.eq('user_id', userId)
.order('last_watched_at', { ascending: false })
.limit(1)
.single();
return {
enrollment,
course: enrollment.course,
progress: {
...progress,
last_watched_lesson: lastLesson?.lesson,
last_watched_at: lastLesson?.last_watched_at,
},
};
})
);
return coursesWithProgress;
}
export async function getCourseProgress(userId, courseId) {
const { data, error } = await supabase.rpc('get_course_progress', {
p_user_id: userId,
p_course_id: courseId
});
if (error) throw error;
return data;
}
Get Course Curriculum
export async function getCourseCurriculum(courseId, userId = null) {
// Get sections with lessons
const { data: sections, error } = await supabase
.from('course_sections')
.select(`
id,
title,
description,
position,
lessons:course_lessons (
id,
title,
description,
youtube_video_id,
duration_seconds,
position,
is_preview
)
`)
.eq('course_id', courseId)
.order('position', { ascending: true });
if (error) throw error;
// If user is logged in, fetch their progress
if (userId) {
for (const section of sections) {
for (const lesson of section.lessons) {
const { data: progress } = await supabase
.from('lesson_progress')
.select('completed, watch_time_seconds')
.eq('user_id', userId)
.eq('lesson_id', lesson.id)
.single();
lesson.progress = progress || { completed: false, watch_time_seconds: 0 };
}
}
}
return sections;
}
Enroll in Course
export async function enrollInCourse(userId, courseId) {
const { data, error } = await supabase
.from('course_enrollments')
.insert({
user_id: userId,
course_id: courseId,
status: 'active',
source: 'web-enroll',
})
.select()
.single();
if (error) {
if (error.code === '23505') { // Unique violation
throw new Error('You are already enrolled in this course');
}
throw error;
}
return data;
}
7. Security Considerations
7.1 Row Level Security (RLS) Policies
Add to migration file:
-- ============================================
-- RLS POLICIES FOR VIDEO COURSE PLATFORM
-- ============================================
-- Enable RLS on all tables
ALTER TABLE course_sections ENABLE ROW LEVEL SECURITY;
ALTER TABLE course_lessons ENABLE ROW LEVEL SECURITY;
ALTER TABLE lesson_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE lesson_notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE admin_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE course_certificates ENABLE ROW LEVEL SECURITY;
-- ============================================
-- ADMIN POLICIES
-- ============================================
-- Admin users: Full access to courses
CREATE POLICY "Admin full access courses"
ON nova_tracks
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
)
);
-- Admin users: Full access to sections
CREATE POLICY "Admin full access sections"
ON course_sections
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
)
);
-- Admin users: Full access to lessons
CREATE POLICY "Admin full access lessons"
ON course_lessons
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
)
);
-- ============================================
-- STUDENT POLICIES - SECTIONS
-- ============================================
-- Students: View sections from enrolled courses
CREATE POLICY "Students view enrolled sections"
ON course_sections
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM course_enrollments
WHERE user_id = auth.uid()
AND course_id = course_sections.course_id
AND status = 'active'
)
);
-- Public: View sections from any course (for preview)
CREATE POLICY "Public view all sections"
ON course_sections
FOR SELECT
TO public
USING (true);
-- ============================================
-- STUDENT POLICIES - LESSONS
-- ============================================
-- Students: View lessons from enrolled courses OR preview lessons
CREATE POLICY "Students view enrolled lessons"
ON course_lessons
FOR SELECT
TO authenticated
USING (
is_preview = TRUE -- Allow previewing free lessons
OR EXISTS (
SELECT 1 FROM course_enrollments
WHERE user_id = auth.uid()
AND course_id = course_lessons.course_id
AND status = 'active'
)
);
-- Public: View only preview lessons
CREATE POLICY "Public view preview lessons"
ON course_lessons
FOR SELECT
TO public
USING (is_preview = TRUE);
-- ============================================
-- STUDENT POLICIES - PROGRESS
-- ============================================
-- Users can only manage their own progress
CREATE POLICY "Users manage own progress"
ON lesson_progress
FOR ALL
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Users can only view their own progress
CREATE POLICY "Users view own progress"
ON lesson_progress
FOR SELECT
TO authenticated
USING (user_id = auth.uid());
-- ============================================
-- STUDENT POLICIES - NOTES
-- ============================================
-- Users can only manage their own notes
CREATE POLICY "Users manage own notes"
ON lesson_notes
FOR ALL
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- ============================================
-- ADMIN USER POLICIES
-- ============================================
-- Only super admins can view admin users
CREATE POLICY "Super admins view admin users"
ON admin_users
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
AND role = 'super_admin'
)
);
-- Only super admins can manage admin users
CREATE POLICY "Super admins manage admin users"
ON admin_users
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_users
WHERE user_id = auth.uid()
AND role = 'super_admin'
)
);
-- ============================================
-- CERTIFICATE POLICIES
-- ============================================
-- Users can view their own certificates
CREATE POLICY "Users view own certificates"
ON course_certificates
FOR SELECT
TO authenticated
USING (user_id = auth.uid());
-- Anyone can verify a certificate by code (public endpoint)
CREATE POLICY "Public verify certificates"
ON course_certificates
FOR SELECT
TO public
USING (true);
-- System can issue certificates (will use service role)
-- No policy needed - service role bypasses RLS
7.2 YouTube URL Validation
Security measures in YouTube service:
// src/services/youtube.js
/**
* Validate YouTube video ID format (prevent XSS)
*/
export function validateYouTubeId(id) {
// YouTube IDs are exactly 11 characters: letters, numbers, underscore, hyphen
return /^[a-zA-Z0-9_-]{11}$/.test(id);
}
/**
* Sanitize YouTube URL before processing
*/
export function sanitizeYouTubeUrl(url) {
// Remove any script tags or suspicious content
const cleaned = url.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// Only allow youtube.com and youtu.be domains
const allowedDomains = ['youtube.com', 'youtu.be', 'www.youtube.com'];
try {
const urlObj = new URL(cleaned);
if (!allowedDomains.includes(urlObj.hostname)) {
throw new Error('Invalid YouTube domain');
}
return cleaned;
} catch {
throw new Error('Invalid URL format');
}
}
7.3 Admin Route Protection
File: src/hooks/useAdminAuth.js
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../lib/supabaseClient';
export function useAdminAuth() {
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
checkAdminStatus();
}, []);
async function checkAdminStatus() {
try {
// Get current user
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
navigate('/login');
return;
}
// Check if user is admin
const { data: adminUser } = await supabase
.from('admin_users')
.select('role')
.eq('user_id', user.id)
.single();
if (!adminUser) {
navigate('/'); // Redirect non-admins
return;
}
setIsAdmin(true);
} catch (error) {
console.error('Admin auth check failed:', error);
navigate('/');
} finally {
setIsLoading(false);
}
}
return { isAdmin, isLoading };
}
Usage in admin pages:
// pages/admin/Dashboard.jsx
import { useAdminAuth } from '../../hooks/useAdminAuth';
export function AdminDashboard() {
const { isAdmin, isLoading } = useAdminAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAdmin) return null; // Will redirect
return <div>Admin Dashboard Content</div>;
}
7.4 Environment Variables Security
Required environment variables:
# .env
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
VITE_YOUTUBE_API_KEY=your-youtube-api-key
Never commit:
- Service role keys (only use server-side)
- Production API keys
- Database passwords
8. Cost Analysis
8.1 Using YouTube Unlisted (Recommended)
Monthly Costs:
✅ Video Hosting: $0 (YouTube)
✅ Video Bandwidth: $0 (YouTube CDN)
✅ Video Transcoding: $0 (automatic)
✅ Video Storage: $0
💰 Supabase Pro: $25/month
- 8 GB database
- 100 GB bandwidth
- 100 GB storage
💰 YouTube Data API: $0
- 10,000 quota units/day (free)
- ~100,000 API requests/month
💰 Domain: $12/year (~$1/month)
💰 Email Service (optional): $10-20/month
- SendGrid, Mailgun, or Resend
TOTAL: $26-46/month
8.2 Self-Hosted Videos Alternative (NOT Recommended)
For comparison only:
💸 Cloudflare R2 Storage:
- $0.015/GB/month storage
- $0.01/GB egress (free tier: 10GB/month)
- 100 courses @ 500MB/course = 50GB
- Storage: $0.75/month
- Bandwidth (1TB/month): $10/month
💸 Video Transcoding:
- AWS MediaConvert: ~$0.015/minute
- 100 courses @ 10 hours = 1,000 hours
- One-time: $900
- Ongoing for new videos: ~$50/month
💸 CDN (Cloudflare): $20/month
💸 Supabase: $25/month
TOTAL: ~$105/month + $900 upfront
🎯 YouTube saves you: $80-600/month!
8.3 Revenue Potential (If Paid Courses)
Example Pricing Model:
Free Tier:
- 10 courses
- Community support
Pro Tier ($29/month or $199/year):
- All courses
- Certificates
- Priority support
- Live Q&A sessions
Enterprise (Custom):
- Custom courses
- Team accounts
- Analytics
Projected Revenue (Year 1):
- 1,000 free users
- 100 pro monthly ($2,900/month)
- 50 pro yearly ($9,950 upfront)
- Total Year 1: ~$45,000
Costs Year 1: ~$500
Profit Margin: 98%+ 🚀
9. Example Data Models
9.1 Sample Course Structure
// COURSE
{
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
slug: "ai-fundamentals",
code: "AI101",
title: "AI Fundamentals for Beginners",
description: "Learn the basics of artificial intelligence and machine learning",
instructor_name: "Dr. Jane Smith",
thumbnail_url: "https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg",
trailer_youtube_id: "dQw4w9WgXcQ",
total_lessons: 24,
estimated_hours: 8.5,
pricing_tier: "free",
status: "open",
created_at: "2025-10-01T00:00:00Z",
updated_at: "2025-10-15T00:00:00Z"
}
9.2 Course Sections & Lessons
// SECTIONS
[
{
id: "section-uuid-1",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
title: "Introduction to AI",
description: "Get started with AI basics",
position: 0,
// LESSONS
lessons: [
{
id: "lesson-uuid-1",
section_id: "section-uuid-1",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
title: "What is AI?",
description: "Understanding artificial intelligence",
youtube_video_id: "abc123xyz",
duration_seconds: 720, // 12 minutes
position: 0,
is_preview: true, // Free preview
resources_text: "Download slides: https://example.com/slides.pdf"
},
{
id: "lesson-uuid-2",
section_id: "section-uuid-1",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
title: "History of AI",
description: "From Turing to modern AI",
youtube_video_id: "def456uvw",
duration_seconds: 900, // 15 minutes
position: 1,
is_preview: false, // Locked for enrolled users only
resources_text: null
}
]
},
{
id: "section-uuid-2",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
title: "Machine Learning Basics",
description: "Introduction to ML concepts",
position: 1,
lessons: [
// More lessons...
]
}
]
9.3 Student Progress Example
// USER ENROLLMENT
{
id: "enrollment-uuid-1",
user_id: "user-uuid-123",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
status: "active",
source: "web-enroll",
created_at: "2025-10-20T10:00:00Z"
}
// LESSON PROGRESS
[
{
id: "progress-uuid-1",
user_id: "user-uuid-123",
lesson_id: "lesson-uuid-1",
enrollment_id: "enrollment-uuid-1",
watch_time_seconds: 720, // Watched full 12 minutes
completed: true,
last_watched_at: "2025-10-20T10:15:00Z"
},
{
id: "progress-uuid-2",
user_id: "user-uuid-123",
lesson_id: "lesson-uuid-2",
enrollment_id: "enrollment-uuid-1",
watch_time_seconds: 450, // Watched 7.5 out of 15 minutes (50%)
completed: false,
last_watched_at: "2025-10-20T10:30:00Z"
}
]
// COURSE PROGRESS SUMMARY (from get_course_progress function)
{
total_lessons: 24,
completed_lessons: 1,
in_progress_lessons: 1,
total_duration_seconds: 14400, // 4 hours
watched_duration_seconds: 1170, // 19.5 minutes
completion_percentage: 4.17,
last_watched_at: "2025-10-20T10:30:00Z"
}
9.4 Certificate Example
{
id: "cert-uuid-1",
user_id: "user-uuid-123",
course_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
certificate_code: "CERT-1KL8M9N-AB12CD",
issued_at: "2025-10-30T14:30:00Z",
completion_percentage: 100.00
}
// Certificate verification URL:
// https://yourplatform.com/verify/CERT-1KL8M9N-AB12CD
10. Next Steps - Decision Points
⚠️ IMPORTANT: Answer These Before Implementation
1. Admin Access Control
Question: Who should have admin access initially?
Options:
- Just you (one super admin)
- Multiple instructors (role-based access)
- Team with different permissions
Decision: Three roles: super_admin, instructor, and viewer. Start with super_admin for initial user.
2. Course Enrollment Strategy
Question: How should students enroll in courses?
Options:
- Auto-enroll on signup (all free courses accessible)
- Manual enrollment per course (click "Enroll" button)
- Waitlist integration (use existing
track_waitlisttable) - Hybrid (auto-enroll in featured courses, manual for others)
Decision: Manual enrollment via "Enroll" button when viewing video. Show enrollment prompt if user tries to watch without enrollment. Waitlist integration for coming-soon courses.
3. Lesson Progression
Question: Should lessons be sequential or free-form?
Options:
- Sequential (must complete lesson 1 before lesson 2)
- Free-form (can skip around)
- Hybrid (sections are sequential, lessons within section are free)
Decision: Hybrid - Lessons within sections are free-form (can skip around). No locking mechanism needed.
4. Completion Threshold
Question: What % watched counts as "completed"?
Options:
- 90% (recommended - allows skipping credits)
- 100% (must watch entire video)
- 80% (more lenient)
Decision: 90% watch time marks lesson as complete (default recommendation)
5. Certificates
Question: Certificate issuance strategy?
Options:
- Auto-issue on 100% completion
- Auto-issue + require final quiz/assessment
- Manual approval by instructor
- No certificates (Phase 6 feature)
Decision: Auto-issue certificate on 100% course completion
If yes to certificates:
- I have a certificate design template
- Create a default template for me
6. Priority Features
Question: Which phases should we implement first?
Must Have (implement first):
- Phase 1: Database Setup
- Phase 2: YouTube Integration
- Phase 3: Admin Panel Core
- Phase 4: Student Dashboard
Nice to Have (implement if time allows):
- Phase 5: Analytics & Polish
- Phase 6: Advanced Features (quiz, forums, etc.)
Decision: MVP first - Phases 1-4 only. Polish and advanced features later.
7. Existing Content Migration
Question: Do you have existing content in nova_tracks to migrate?
- Yes - I have ___ existing tracks to convert to video courses
- No - Starting fresh
If yes:
- Migrate existing tracks to new course structure
- Keep old structure, start new alongside
Decision: No migration needed. Starting with fresh video course structure.
8. Branding & Design
Question: Design preferences?
- Match existing site design
- Create new modern design
- Use component library (shadcn/ui, Material UI, etc.)
Preferred color scheme:
- Current site colors
- Dark mode primary
- Light mode primary
- Auto (system preference)
Decision: Match existing site design and color scheme for consistency.
11. Progress Tracker
Database Setup ✅
- Create migration file
- Add course_sections table
- Add course_lessons table
- Add lesson_progress table
- Add lesson_notes table
- Add admin_users table
- Add course_certificates table
- Add indexes
- Add RLS policies
- Add database functions (get_course_progress, get_course_analytics)
- Test with sample data
- Verify RLS works
YouTube Integration ✅
- Get YouTube Data API key
- Add to environment variables
- Create youtube.js service
- Implement extractVideoId()
- Implement getVideoMetadata()
- Implement formatDuration()
- Create VideoPlayer component
- Test video embedding
- Test progress tracking
Admin Panel ✅
- Create admin routes
- Create AdminLayout component
- Create useAdminAuth hook
- Dashboard page
- CourseList page
- CourseEditor page
- SectionManager component
- LessonManager component
- YouTubeVideoInput component
- Drag-and-drop reordering
- Test full course creation flow
Student Dashboard ✅
- Create dashboard routes
- Create DashboardLayout
- MyCourses page
- CourseCard component
- CourseDetail page
- LearningPage (video player)
- LessonSidebar component
- Progress tracking implementation
- LessonNotes component
- Progress page
- Test enrollment flow
- Test video watching flow
- Test progress saves correctly
Analytics & Polish ✅
- Admin analytics dashboard
- Enrollment charts
- Completion rate charts
- StudentTable component
- Individual student progress view
- Certificate generation
- Certificate design
- Email notifications
- Search/filter functionality
- Mobile responsive
- Performance optimization
Advanced Features (Optional)
- Quiz system
- Discussion forums
- Downloadable resources
- Course reviews/ratings
- Student messaging
- Live session integration
- Assignments
- Gamification
Implementation Notes
Last Updated: 2025-10-30 Current Phase: Phase 1 - Database Setup Next Action: Create database migration file
Notes:
- All decision points answered and documented
- Starting with MVP (Phases 1-4)
- Three admin roles: super_admin, instructor, viewer
- Manual enrollment with free-form lesson progression
- 90% watch time = lesson complete
- Auto-issue certificates on 100% completion
- Match existing site design
Resources & References
Documentation
Design Inspiration
- Coursera
- Udemy
- Skillshare
- Frontend Masters
Tools
End of Plan