Ryan Trann
February 7, 2026
7 min read

Video is quickly becoming the format of choice for content on the internet. To create an MVP with user-generated video uploads, you not only need to allow users to upload files, you also need to store them and let users play them back. This guide shows how to upload videos using Next.js with Supabase or Firebase storage, a CDN URL, and a database table to track videos.
First time with Supabase or Firebase?
To add video to your app MVP, you need a strong user-generated content (UGC) video flow. This flow requires five key parts:
<video> elementDatabases store the metadata you'll need when it's time to programmatically get the right file for the right situation. Metadata such as public_url, id, user_id etc.
Let's look at two common database examples.
Create videos table
sqlcreate table videos (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id),
storage_path text NOT null,
public_url text NOT null,
mime_type text,
size_bytes bigint,
created_at timestamptz default now()
);Firestore document creation
tsximport { doc, setDoc } from "firebase/firestore";
await setDoc(doc(db, "videos", crypto.randomUUID()), {
userId,
storagePath: path,
publicUrl: url,
mimeType: file.type,
sizeBytes: file.size,
createdAt: Date.now(),
});Before implementing uploads, you should decide where the video files will live and how they'll be served to clients. Generally speaking, this means cloud object storage backed by a CDN.
Both Supabase Storage and Firebase Storage provide the core features for video uploads:
If you're hosting and serving videos, either option can work for you.
The right choice depends less on "CDN features" and more on whether your video pipeline is backend driven or client driven.
The core flow is: the user selects a file, you upload it to storage, and then you write the metadata entry to your database.
Below are both Supabase and Firebase implementations, including direct uploads and signed upload URLs.
Client component
tsx"use client";
import { useState } from "react";
export default function UploadPage() {
const [file, setFile] = useState<File | null>(null);
async function handleUpload() {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
await fetch("/api/upload", { method: "POST", body: formData });
}
return (
<div>
<input type="file" accept="video/*" onChange={e => setFile(e.target.files?.[0] ?? null)} />
<button onClick={handleUpload}>Upload</button>
</div>
);
}API route
tsximport { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
export async function POST(req: NextRequest) {
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
const data = await req.formData();
const file = data.get("file") as File;
const extension = file.name.split(".").pop();
const videoId = crypto.randomUUID();
const path = `videos/${videoId}.${extension}`;
const { error } = await supabase.storage
.from("videos")
.upload(path, file, { contentType: file.type });
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
const url = `${process.env.SUPABASE_URL}/storage/v1/object/public/${path}`;
const { error: dbError } = await supabase.from("videos").insert({
id: videoId,
user_id: "some-user-id",
storage_path: path,
public_url: url,
mime_type: file.type,
size_bytes: file.size,
});
if (dbError) {
return NextResponse.json({ error: dbError.message }, { status: 500 });
}
return NextResponse.json({ url, path });
}API route for signed URL
tsxexport async function POST() {
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
const videoId = crypto.randomUUID();
const path = `videos/${videoId}.mp4`;
const { data, error } = await supabase.storage.from("videos").createSignedUploadUrl(path);
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
return NextResponse.json({ uploadUrl: data.signedUrl, path, videoId });
}Client upload with signed URL
tsxawait fetch(uploadUrl, { method: "PUT", body: file });Insert metadata after upload
tsxawait supabase.from("videos").insert({
id: videoId,
user_id,
storage_path: path,
public_url: `${process.env.SUPABASE_URL}/storage/v1/object/public/${path}`,
mime_type: file.type,
size_bytes: file.size,
});Firebase handles everything client-side.
Client upload (Firebase Storage)
tsximport { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const videoId = crypto.randomUUID()
const path = `videos/${videoId}`;
const storageRef = ref(storage, path);
const task = uploadBytesResumable(storageRef, file);
task.on("state_changed", null, console.error, async () => {
const url = await getDownloadURL(task.snapshot.ref);
await setDoc(doc(db, "videos", crypto.randomUUID()), {
id: videoId,
userId,
storagePath: path,
publicUrl: url,
mimeType: file.type,
sizeBytes: file.size,
createdAt: Date.now(),
});
});Once you have a video URL:
Basic video element
html<video
src={url}
controls
playsInline
/>For looping autoplay:
Autoplay video
html<video
src={url}
controls
autoplay
muted
loop
playsInline
/>File size validation
tsxif (file.size > 200 * 1024 * 1024) {
alert("Max size is 200MB");
return;
}MIME type validation
tsxif (!file.type.startsWith("video/")) {
alert("File must be a video");
return;
}This step requires ffmpeg, which can be tricky to set up properly in a Node.js environment. I recommend you skip this early on and add it later when you've got time to invest. Thumbnail images or posters can help improve user experience by giving users the feeling that the interface is ready in real time while the video loads.
Client → API or Signed URL → Storage Bucket → CDN → DB record → <video> playback
That's the MVP video pipeline for adding user-generated video to a Next.js application without dealing with transcoding, queues, workers and distributed architecture.
This system won't last long as an MVP. If you need something reliable, performant, and scalable, try Hyperserve. We have done the hard work of building Video Architecture at Scale and wrapped it in a simple API. Sign up for a free account so adding video can be as easy as image uploads.
Video Hosting API made simple.
© 2026 Hyperserve. All rights reserved.
Made by Misty Mountain Software