Skip to main content
RapidDev - Software Development Agency

How to Build Video streaming backend with V0

Build a video streaming platform with V0 featuring Mux Direct Uploads for transcoding and HLS adaptive streaming, chunked uploads with @mux/upchunk for progress tracking, webhook-driven status updates, and a video feed with viewer analytics. You'll handle large file uploads without hitting Vercel's 4.5MB body limit — all in about 2-4 hours.

What you'll build

  • Video upload page with @mux/upchunk for chunked Direct Uploads and shadcn/ui Progress bar
  • Mux webhook handler that updates video status to 'ready' and sets playback_id when transcoding completes
  • Video feed page with Card grid showing thumbnails, durations, view counts, and processing status Badge
  • Video player page using @mux/mux-player-react with AspectRatio container for adaptive HLS streaming
  • Playlist management with drag-and-drop ordering and Tabs for videos/playlists
  • View tracking API that logs watch duration and completion for per-video analytics
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced10 min read2-4 hoursV0 Premium (Supabase + Mux integrations)April 2026RapidDev Engineering Team
TL;DR

Build a video streaming platform with V0 featuring Mux Direct Uploads for transcoding and HLS adaptive streaming, chunked uploads with @mux/upchunk for progress tracking, webhook-driven status updates, and a video feed with viewer analytics. You'll handle large file uploads without hitting Vercel's 4.5MB body limit — all in about 2-4 hours.

What you're building

Video platforms need to handle large file uploads, transcode videos into multiple quality levels, and stream adaptively based on the viewer's connection speed. Building this from scratch with FFmpeg would take weeks. Mux handles all of it through their API.

V0 generates the upload interface, video player, feed, and analytics dashboard from prompts. Mux processes uploaded videos into HLS adaptive streams with automatic thumbnail generation. Supabase stores video metadata and view analytics. The client uploads directly to Mux using chunked uploads, completely bypassing Vercel's 4.5MB serverless body limit.

The architecture uses an API route to create Mux Direct Upload URLs, a webhook handler for transcoding completion events, @mux/mux-player-react for the player, and Server Components for the video feed and analytics.

Final result

A video streaming platform with chunked uploads, adaptive HLS playback, processing status tracking, playlist management, and viewer analytics.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase & Auth
MuxVideo Processing & Streaming

Prerequisites

  • A V0 account (Premium recommended for the project complexity)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • A Mux account with API Access Token (Token ID + Token Secret)
  • No additional services needed — Mux handles transcoding, storage, and CDN delivery

Build steps

1

Set up the videos, views, and playlists database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the tables for videos, view tracking, playlists, and playlist-video relationships.

supabase/migrations/001_schema.sql
1CREATE TABLE videos (
2 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3 owner_id uuid NOT NULL,
4 title text NOT NULL,
5 description text,
6 thumbnail_url text,
7 status text DEFAULT 'processing'
8 CHECK (status IN ('processing','ready','failed','archived')),
9 duration_seconds int,
10 views_count int DEFAULT 0,
11 upload_id text,
12 playback_id text,
13 created_at timestamptz DEFAULT now()
14);
15
16CREATE TABLE video_views (
17 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
18 video_id uuid REFERENCES videos(id) ON DELETE CASCADE,
19 viewer_id uuid,
20 watch_duration_seconds int DEFAULT 0,
21 completed boolean DEFAULT false,
22 created_at timestamptz DEFAULT now()
23);
24
25CREATE TABLE playlists (
26 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
27 owner_id uuid NOT NULL,
28 title text NOT NULL,
29 is_public boolean DEFAULT true,
30 created_at timestamptz DEFAULT now()
31);
32
33CREATE TABLE playlist_videos (
34 playlist_id uuid REFERENCES playlists(id) ON DELETE CASCADE,
35 video_id uuid REFERENCES videos(id) ON DELETE CASCADE,
36 position int NOT NULL,
37 PRIMARY KEY (playlist_id, video_id)
38);
39
40CREATE OR REPLACE FUNCTION increment_views(p_video_id uuid)
41RETURNS void AS $$
42 UPDATE videos SET views_count = views_count + 1
43 WHERE id = p_video_id;
44$$ LANGUAGE sql;

Pro tip: Use V0's prompt queuing — queue the schema, upload page, video player, and feed page as four separate prompts while you set up the Mux account.

Expected result: Four tables created with a views counter RPC function. The videos table stores Mux playback_id for streaming and status for processing state.

2

Create the Mux Direct Upload endpoint

Build an API route that creates a Mux Direct Upload URL. The client will upload video chunks directly to Mux using this URL, completely bypassing your server and Vercel's body size limit.

app/api/mux/upload/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Mux from '@mux/mux-node'
3import { createClient } from '@/lib/supabase/server'
4
5const mux = new Mux({
6 tokenId: process.env.MUX_TOKEN_ID!,
7 tokenSecret: process.env.MUX_TOKEN_SECRET!,
8})
9
10export async function POST(req: NextRequest) {
11 const supabase = await createClient()
12 const { title, description } = await req.json()
13
14 const user = (await supabase.auth.getUser()).data.user
15 if (!user) {
16 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
17 }
18
19 const upload = await mux.video.uploads.create({
20 cors_origin: process.env.NEXT_PUBLIC_APP_URL ?? '*',
21 new_asset_settings: {
22 playback_policy: ['public'],
23 encoding_tier: 'baseline',
24 },
25 })
26
27 const { data: video } = await supabase
28 .from('videos')
29 .insert({
30 owner_id: user.id,
31 title,
32 description,
33 upload_id: upload.id,
34 })
35 .select()
36 .single()
37
38 return NextResponse.json({
39 videoId: video?.id,
40 uploadUrl: upload.url,
41 })
42}

Pro tip: Set cors_origin to your production domain in Mux upload settings. Using '*' works for development but should be restricted in production for security.

Expected result: The API returns a Mux Direct Upload URL. The client uses this URL with @mux/upchunk to upload video files of any size directly to Mux.

3

Build the Mux webhook handler for transcoding events

Create a webhook endpoint that Mux calls when video processing completes. It updates the video status and stores the playback_id for streaming.

app/api/webhooks/mux/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import crypto from 'crypto'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10export async function POST(req: NextRequest) {
11 const rawBody = await req.text()
12 const signature = req.headers.get('mux-signature') ?? ''
13
14 const expectedSig = crypto
15 .createHmac('sha256', process.env.MUX_WEBHOOK_SECRET!)
16 .update(rawBody)
17 .digest('hex')
18
19 if (signature !== `sha256=${expectedSig}`) {
20 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
21 }
22
23 const event = JSON.parse(rawBody)
24
25 if (event.type === 'video.asset.ready') {
26 const asset = event.data
27 const playbackId = asset.playback_ids?.[0]?.id
28
29 await supabase
30 .from('videos')
31 .update({
32 status: 'ready',
33 playback_id: playbackId,
34 duration_seconds: Math.round(asset.duration ?? 0),
35 thumbnail_url: `https://image.mux.com/${playbackId}/thumbnail.webp`,
36 })
37 .eq('upload_id', asset.upload_id)
38 }
39
40 if (event.type === 'video.asset.errored') {
41 await supabase
42 .from('videos')
43 .update({ status: 'failed' })
44 .eq('upload_id', event.data.upload_id)
45 }
46
47 return NextResponse.json({ received: true })
48}

Pro tip: Always use request.text() instead of request.json() for webhook handlers. Signature verification requires the raw body string. Parsing to JSON first changes the string representation.

Expected result: When Mux finishes transcoding, the webhook updates the video status to 'ready' and stores the playback_id. Failed transcodes are marked as 'failed'.

4

Build the upload page with chunked upload progress

Create the upload form with @mux/upchunk for chunked file uploads. The client uploads directly to Mux, showing a Progress bar, without any data passing through your server.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a video upload page at app/upload/page.tsx with:
3// 1. Client component ('use client') with shadcn/ui Input for title, Textarea for description
4// 2. File input accepting video/* files
5// 3. On submit: POST to /api/mux/upload to get the upload URL, then use @mux/upchunk UpChunk.createUpload() to send the file in chunks
6// 4. shadcn/ui Progress bar showing upload percentage from upchunk's 'progress' event
7// 5. Status text: 'Uploading...' → 'Processing...' → redirect to video page when ready
8// 6. Error handling for failed uploads with Alert component
9// 7. File size display and video preview before upload
10// Install @mux/upchunk as a dependency.

Expected result: An upload page where selecting a video file and clicking Upload sends chunks directly to Mux via the Direct Upload URL, with a Progress bar tracking completion percentage.

5

Build the video player and feed pages

Create the video player page with @mux/mux-player-react for adaptive HLS streaming, and the video feed page showing all ready videos in a Card grid.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build two pages:
3// 1. app/videos/[id]/page.tsx — Video player page:
4// - Server Component fetching video by ID from Supabase
5// - @mux/mux-player-react MuxPlayer with playbackId, in shadcn/ui AspectRatio (16:9)
6// - Title, description, view count, upload date below player
7// - Client component wrapper that POSTs to /api/views on play events
8// 2. app/videos/page.tsx — Video feed:
9// - Server Component fetching all videos WHERE status = 'ready'
10// - Grid of shadcn/ui Cards with Mux thumbnail (image.mux.com/{playbackId}/thumbnail.webp)
11// - Duration overlay, view count, title, upload date
12// - Badge for 'processing' videos (show skeleton Card)
13// - Pagination with limit 12 per page
14// Install @mux/mux-player-react as a dependency.

Expected result: A video feed page with thumbnail Cards linking to individual player pages. The player uses Mux's adaptive HLS streaming, automatically adjusting quality based on the viewer's connection.

Complete code

app/api/webhooks/mux/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import crypto from 'crypto'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10export async function POST(req: NextRequest) {
11 const rawBody = await req.text()
12 const signature = req.headers.get('mux-signature') ?? ''
13
14 const expectedSig = crypto
15 .createHmac('sha256', process.env.MUX_WEBHOOK_SECRET!)
16 .update(rawBody)
17 .digest('hex')
18
19 if (signature !== `sha256=${expectedSig}`) {
20 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
21 }
22
23 const event = JSON.parse(rawBody)
24
25 if (event.type === 'video.asset.ready') {
26 const asset = event.data
27 const playbackId = asset.playback_ids?.[0]?.id
28
29 await supabase
30 .from('videos')
31 .update({
32 status: 'ready',
33 playback_id: playbackId,
34 duration_seconds: Math.round(asset.duration ?? 0),
35 thumbnail_url: `https://image.mux.com/${playbackId}/thumbnail.webp`,
36 })
37 .eq('upload_id', asset.upload_id)
38 }
39
40 if (event.type === 'video.asset.errored') {
41 await supabase
42 .from('videos')
43 .update({ status: 'failed' })
44 .eq('upload_id', event.data.upload_id)
45 }
46
47 return NextResponse.json({ received: true })
48}

Customization ideas

Add video chapters

Create a chapters table with timestamps and titles. Display chapter markers on the Mux player timeline and a chapter list below the video for easy navigation.

Add comments and reactions

Build a threaded comment system below videos with real-time updates via Supabase Realtime. Add emoji reactions that appear as floating overlays on the player.

Add video transcription

Use Mux's auto-generated captions or integrate with Deepgram/Whisper API for transcription. Display captions on the player and make videos searchable by transcript content.

Add monetization with Stripe

Gate premium videos behind a Stripe subscription. Check subscription status in middleware before rendering the player page, redirecting non-subscribers to a pricing page.

Common pitfalls

Pitfall: Uploading video files through your API route instead of using Direct Uploads

How to avoid: Use Mux Direct Uploads. Your API creates a signed upload URL, then the client uploads chunks directly to Mux using @mux/upchunk. Your server never touches the video file.

Pitfall: Using request.json() in the webhook handler

How to avoid: Always use request.text() to get the raw body for HMAC verification, then parse with JSON.parse() after verification succeeds.

Pitfall: Polling for video status instead of using webhooks

How to avoid: Register a webhook URL in the Mux dashboard. Mux sends video.asset.ready when transcoding completes. The webhook handler updates the database immediately.

Pitfall: Exposing MUX_TOKEN_SECRET in client-side code

How to avoid: Set MUX_TOKEN_ID and MUX_TOKEN_SECRET in V0's Vars tab without NEXT_PUBLIC_ prefix. Only the Direct Upload URL (a signed, temporary URL) is sent to the client.

Best practices

  • Use Mux Direct Uploads with @mux/upchunk to bypass Vercel's 4.5MB serverless body limit entirely
  • Always verify Mux webhook signatures using request.text() for the raw body and HMAC-SHA256
  • Set MUX_TOKEN_ID, MUX_TOKEN_SECRET, and MUX_WEBHOOK_SECRET in V0's Vars tab (server-only, no NEXT_PUBLIC_ prefix)
  • Use Mux's thumbnail URL pattern (image.mux.com/{playbackId}/thumbnail.webp) for automatic thumbnail generation
  • Show a Processing Badge and Skeleton card for videos still being transcoded to set user expectations
  • Log watch duration with periodic POSTs (every 30 seconds) rather than only on video end, to capture partial views accurately
  • Use encoding_tier 'baseline' for faster transcoding during development, switch to 'smart' for production quality

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a video streaming platform with Next.js App Router, Supabase, and Mux. I need: 1) Mux Direct Upload URL creation in an API route, 2) Client-side chunked uploads with @mux/upchunk showing progress, 3) A webhook handler for video.asset.ready that updates Supabase with the playback_id, 4) The @mux/mux-player-react component for adaptive HLS streaming. Help me design the upload-to-playback pipeline.

Build Prompt

Create a Mux webhook handler at app/api/webhooks/mux/route.ts that: 1) Reads the raw body with request.text(), 2) Verifies the mux-signature header using HMAC-SHA256 with MUX_WEBHOOK_SECRET, 3) Handles video.asset.ready by updating the Supabase videos table with playback_id, duration_seconds, thumbnail_url, and status='ready', 4) Handles video.asset.errored by setting status='failed', 5) Uses the service role key to bypass RLS.

Frequently asked questions

Why use Mux instead of processing video myself?

Mux handles transcoding into multiple quality levels (HLS adaptive streaming), CDN delivery, thumbnail generation, and player optimization. Building this with FFmpeg on Vercel serverless is impractical due to timeout limits, memory constraints, and the complexity of adaptive bitrate encoding.

How does the chunked upload work?

@mux/upchunk splits large video files into small chunks and uploads them directly to Mux's Direct Upload URL. Your server only creates the upload URL — the actual video data goes directly from the browser to Mux, bypassing Vercel's 4.5MB body limit. The upchunk library emits progress events for the Progress bar.

What happens during video processing?

After upload, Mux transcodes the video into multiple quality renditions (360p through 1080p+) for adaptive streaming. This takes 30 seconds to several minutes depending on video length. The video.asset.ready webhook fires when all renditions are complete.

What V0 plan do I need?

V0 Premium ($20/month) is recommended. The video platform involves multiple pages, API routes, and webhook handlers that require several prompt iterations. Mux has a free tier with 10GB storage and 20 minutes of video.

How much does Mux cost?

Mux pricing is usage-based: video encoding at $0.015/minute, storage at $0.007/GB/month, and streaming delivery at $0.00075/minute viewed. The free tier includes 10GB storage and is sufficient for development and small projects.

How do I deploy this?

Click Share then Publish in V0. Set MUX_TOKEN_ID, MUX_TOKEN_SECRET, and MUX_WEBHOOK_SECRET in V0's Vars tab (no NEXT_PUBLIC_ prefix). After deploying, register the webhook URL in the Mux dashboard: https://your-domain.vercel.app/api/webhooks/mux.

Can RapidDev help build a custom video platform?

Yes. RapidDev has built 600+ apps including video streaming platforms with content gating, multi-tenant architectures, and analytics dashboards. Book a free consultation to discuss your video platform requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.