Hi Friends!
Last Week, I came across an incredible tutorial by Beau Carnes on freeCodeCamp. He built a full Loom clone using Next.js 15 and Mux, complete with screen recording, AI transcripts, automatic watermarking, and signed playback URLs.
But there was one thing missing. Every user could see every video, and my brain started thinking about building apps like these for businesses. If I were to create something closer to production-ready, what would that look like?
Well, there were no real user accounts, no auth, nothing stopping you from watching someone else’s recordings. So that’s a start.
So I decided to fix that and changed some colors around. I added full user authentication, private video libraries, and made sure every video is locked to the person who recorded it. If you need to learn app security, this is for you, plus everything is free to use.
Here’s how I did it, file by file... but if you want to multi-task while listening to me explain this, feel free to watch it here:
https://youtu.be/AoqVE9V1CGw
-------------------------------------------------
The Stack
Beau’s tutorial gave us Next.js 15, Mux, and a clean screen recorder built on the browser’s MediaRecorder API.
What I added on top was Supabase. If you haven’t used it, think of it as Firebase but with a real Postgres database and a much better developer experience. It handles both authentication and the database, so I only had to add one tool to solve two problems.
Full stack: Next.js 16, Mux, Supabase, TypeScript, Tailwind, and you can deploy it on Vercel.
Github Link here if you want to clone the repo: https://github.com/Fabianamichelle/Screen-Recorder
-------------------------------------------------
Setting Up the Database
Before writing any code, I created a videos table in Supabase:
Two things worth highlighting here.
First, the user_id column links directly to Supabase’s built-in auth table. Every video is tied to a real user account at the database level.
Second, Row Level Security. This one SQL policy means that no matter what query you run against this table, Supabase will only return rows where the user_id logged-in user matches. The security is enforced at the database level, not just in your app code. That’s a big deal.
-------------------------------------------------
The Supabase Client Files
It’s important that you know that Supabase needs two separate client files in Next.js.
The first is lib/supabase.js, the browser client for client components like the login page:
``` Javascript
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
}
```
The second is lib/supabase-server.js, for server components and server actions. This one imports next/headers to read and write session cookies, which is why it has to be separate. If you mix them into one file, your client components will crash trying to import server-only code.
-------------------------------------------------
Route Protection with proxy.js
I created proxy.js at the root of the project. This runs before every single page load and checks if the user is logged in.
The logic is simple: if there’s no logged-in user and they’re trying to visit a protected route, redirect them to /login. If there is a logged-in user and they try to visit /login, redirect them to /dashboard.
The matcher at the bottom of the file tells Next.js which routes to apply this to. The login page is intentionally left out so logged-out users can actually reach it.
Note: I used proxy.js it instead of the older middleware.js convention because Next.js updated how route interception works.
code.
-------------------------------------------------
The Login Page
The login page handles both signing up and signing in from a single form. A simple isSignUp state variable toggles between the two modes.
The one thing I want to call out is the sign-up flow. After a user signs up, I don’t redirect them to the dashboard. Instead, I show a message asking them to check their email first. That’s because Supabase requires email confirmation before an account is active. I display that message in green so it doesn’t look like an error.
There’s also a small auth callback route at app/auth/callback/route.js. When Supabase sends a confirmation email, the link in that email points to this route. It grabs the one-time code from the URL, exchanges it for a real session, and redirects the user to their dashboard. Without this file, the confirmation link would just 404.
-------------------------------------------------
Saving Videos to Supabase
This is the most important change. After a video finishes processing on Mux, I added a saveVideo server action that stores the playback ID, asset ID, and user ID together in Supabase:
``` Javascript
export async function saveVideo({ muxAssetId, muxPlaybackId, title }) {
const supabase = await createServerSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Not authenticated')
await supabase.from('videos').insert({
user_id: user.id,
mux_asset_id: muxAssetId,
mux_playback_id: muxPlaybackId,
title: title || 'Untitled Recording',
})
}
```
Inside the ScreenRecorder component, I call this right after Mux finishes processing, before redirecting to the video page. That one extra function call is what ties everything together.
-------------------------------------------------
Fetching Only the User’s Videos
Beau’s original dashboard fetched all videos directly from Mux, so everyone could see everything. I replaced that with a listUserVideos function that queries Supabase instead:
``` Javascript
export async function listUserVideos() {
const supabase = await createServerSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return []
const { data } = await supabase
.from('videos')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
return data
}
```
Because of the Row Level Security policy we set up earlier, even if someone tried to query all videos directly, Supabase would only return their own. The app code and the database both enforce the same rule independently.
-------------------------------------------------
Sign Out
I added a sign-out button to the dashboard using a Next.js server action:
```Javascript
async function signOut() {
'use server'
const supabase = await createServerSupabaseClient()
await supabase.auth.signOut()
redirect('/login')
}
```
The form in the JSX calls this directly with action={signOut}. No API route needed, which is one of the things I genuinely love about Next.js server actions.
-------------------------------------------------
One Bonus Feature: Free Downloads
While building this, I noticed that Loom doesn’t let you download videos without upgrading to a paid plan. Since Mux supports MP4 downloads out of the box, my version has free downloads for everyone with no plan required. Small thing, but a nice win.
What I Learned
The biggest lesson was splitting the Supabase client into two files early. I hit that error in development, and it cost me time. If you’re building something similar, create supabase.js for the browser and supabase-server.js for the server from the start.
The second lesson was Row Level Security. It’s one of those features that feels like extra work until you realise it means your app is secure even if there’s a bug in your query logic. I’d use it on every project now.
Either way, this is definitely a step towards production. If you want, you can deploy it on Vercel.
-------------------------------------------------
The Full Stack
Next.js 16 (App Router, Server Components, Server Actions)
Mux (video encoding, streaming, thumbnails, AI transcripts)
Supabase (Postgres database, authentication, row-level security)
TypeScript
Tailwind CSS
-------------------------------------------------
I can’t wait to see what you build!
Let’s Build It Beautifully,
Fab