# How to Build Video streaming backend with V0

- Tool: v0
- Difficulty: Advanced
- Compatibility: V0 Premium (Supabase + Mux integrations)
- Last updated: April 2026

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

## Before you start

- 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

## Step-by-step guide

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

```
CREATE TABLE videos (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id uuid NOT NULL,
  title text NOT NULL,
  description text,
  thumbnail_url text,
  status text DEFAULT 'processing'
    CHECK (status IN ('processing','ready','failed','archived')),
  duration_seconds int,
  views_count int DEFAULT 0,
  upload_id text,
  playback_id text,
  created_at timestamptz DEFAULT now()
);

CREATE TABLE video_views (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  video_id uuid REFERENCES videos(id) ON DELETE CASCADE,
  viewer_id uuid,
  watch_duration_seconds int DEFAULT 0,
  completed boolean DEFAULT false,
  created_at timestamptz DEFAULT now()
);

CREATE TABLE playlists (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id uuid NOT NULL,
  title text NOT NULL,
  is_public boolean DEFAULT true,
  created_at timestamptz DEFAULT now()
);

CREATE TABLE playlist_videos (
  playlist_id uuid REFERENCES playlists(id) ON DELETE CASCADE,
  video_id uuid REFERENCES videos(id) ON DELETE CASCADE,
  position int NOT NULL,
  PRIMARY KEY (playlist_id, video_id)
);

CREATE OR REPLACE FUNCTION increment_views(p_video_id uuid)
RETURNS void AS $$
  UPDATE videos SET views_count = views_count + 1
  WHERE id = p_video_id;
$$ 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.

```
import { NextRequest, NextResponse } from 'next/server'
import Mux from '@mux/mux-node'
import { createClient } from '@/lib/supabase/server'

const mux = new Mux({
  tokenId: process.env.MUX_TOKEN_ID!,
  tokenSecret: process.env.MUX_TOKEN_SECRET!,
})

export async function POST(req: NextRequest) {
  const supabase = await createClient()
  const { title, description } = await req.json()

  const user = (await supabase.auth.getUser()).data.user
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const upload = await mux.video.uploads.create({
    cors_origin: process.env.NEXT_PUBLIC_APP_URL ?? '*',
    new_asset_settings: {
      playback_policy: ['public'],
      encoding_tier: 'baseline',
    },
  })

  const { data: video } = await supabase
    .from('videos')
    .insert({
      owner_id: user.id,
      title,
      description,
      upload_id: upload.id,
    })
    .select()
    .single()

  return NextResponse.json({
    videoId: video?.id,
    uploadUrl: upload.url,
  })
}
```

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

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

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

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

  const expectedSig = crypto
    .createHmac('sha256', process.env.MUX_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex')

  if (signature !== `sha256=${expectedSig}`) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(rawBody)

  if (event.type === 'video.asset.ready') {
    const asset = event.data
    const playbackId = asset.playback_ids?.[0]?.id

    await supabase
      .from('videos')
      .update({
        status: 'ready',
        playback_id: playbackId,
        duration_seconds: Math.round(asset.duration ?? 0),
        thumbnail_url: `https://image.mux.com/${playbackId}/thumbnail.webp`,
      })
      .eq('upload_id', asset.upload_id)
  }

  if (event.type === 'video.asset.errored') {
    await supabase
      .from('videos')
      .update({ status: 'failed' })
      .eq('upload_id', event.data.upload_id)
  }

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

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

```
// Paste this prompt into V0's AI chat:
// Build a video upload page at app/upload/page.tsx with:
// 1. Client component ('use client') with shadcn/ui Input for title, Textarea for description
// 2. File input accepting video/* files
// 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
// 4. shadcn/ui Progress bar showing upload percentage from upchunk's 'progress' event
// 5. Status text: 'Uploading...' → 'Processing...' → redirect to video page when ready
// 6. Error handling for failed uploads with Alert component
// 7. File size display and video preview before upload
// 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.

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

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

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

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

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

  const expectedSig = crypto
    .createHmac('sha256', process.env.MUX_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex')

  if (signature !== `sha256=${expectedSig}`) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(rawBody)

  if (event.type === 'video.asset.ready') {
    const asset = event.data
    const playbackId = asset.playback_ids?.[0]?.id

    await supabase
      .from('videos')
      .update({
        status: 'ready',
        playback_id: playbackId,
        duration_seconds: Math.round(asset.duration ?? 0),
        thumbnail_url: `https://image.mux.com/${playbackId}/thumbnail.webp`,
      })
      .eq('upload_id', asset.upload_id)
  }

  if (event.type === 'video.asset.errored') {
    await supabase
      .from('videos')
      .update({ status: 'failed' })
      .eq('upload_id', event.data.upload_id)
  }

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

## Common mistakes

- **Uploading video files through your API route instead of using Direct Uploads** — Vercel serverless functions have a 4.5MB body limit. A 100MB video will fail immediately. Even if you increase it, streaming large files through your server wastes bandwidth and adds latency. Fix: 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.
- **Using request.json() in the webhook handler** — Webhook signature verification requires the raw body string. JSON.parse then JSON.stringify changes whitespace and key ordering, producing a different hash. Fix: Always use request.text() to get the raw body for HMAC verification, then parse with JSON.parse() after verification succeeds.
- **Polling for video status instead of using webhooks** — Polling the Mux API every few seconds wastes API calls and adds latency between transcoding completion and UI update. Mux rate-limits aggressive polling. Fix: Register a webhook URL in the Mux dashboard. Mux sends video.asset.ready when transcoding completes. The webhook handler updates the database immediately.
- **Exposing MUX_TOKEN_SECRET in client-side code** — The Mux token secret allows creating, deleting, and managing all assets in your account. Exposing it lets anyone delete all your videos. Fix: 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

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

---

Source: https://www.rapidevelopers.com/how-to-build-v0/video-streaming-backend
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/video-streaming-backend
