Back to blogYouTube Video

Published September 4, 2026

Building My Own Self Hosted Loom Part 2

Can't play the video or having issues? Here's the direct link.

AI Summary

The creator completes a self-hosted Loom alternative by solving the video delivery bottleneck without transferring their domain to Cloudflare. They deploy a Cloudflare Worker with an R2 bucket binding to proxy video requests, bypassing the need for Cloudflare CDN domain delegation while retaining edge caching. The Next.js frontend is stripped to a minimal dark/light-mode-compatible UI: a clean video page with keyboard shortcuts, a Calendly CTA, and zero vendor branding. For analytics, they implement a Google Sheets-backed tracker (page views, plays, completions) using OAuth credentials stored locally and git-ignored, avoiding a traditional database. Timestamped chapters are generated via a two-step AI pipeline: audio transcription (using a personal Phantom Edit tool or Groq's free Whisper API) feeds OpenRouter's free Gemma/Nemotron models to output structured WebVTT chapters, which are uploaded alongside the video to R2. The upload script automates compression, transcription, chapter generation, and dual-file upload. Final deployment to Vercel with environment variables yields a functional, low-cost, AI-assisted video platform.

Key Takeaways

  • Use a Cloudflare Worker as a lightweight proxy to serve R2 bucket files without moving your domain's DNS to Cloudflare, preserving Vercel deployment while gaining edge caching.
  • Generate the Worker code via LLM (e.g., Gemini) by prompting for a basic R2 fetch-and-forward script; bind the R2 bucket in the Worker's settings.
  • Replace a full database with Google Sheets for simple analytics: enable Sheets API in Google Cloud, use OAuth (client_secret.json + token.json), store credentials in a git-ignored folder, and write append-only tracking for page views, plays, and completions.
  • Fix authentication scope errors by running a one-time OAuth re-auth script (e.g., `bun run auth-sheets`) and restarting the dev server.
  • Automate chapter generation in the upload pipeline: transcribe audio (Whisper via Groq or custom tool) → feed transcript to a free LLM on OpenRouter (Gemma 4 / Nemotron 3 Ultra) with structured output → format as WebVTT → upload .vtt file to R2 with same basename as video.
  • Switch AI providers when hitting rate limits (e.g., Google's Gemma 4 → Nvidia's Nemotron 3 Ultra) without changing the integration code.
  • Enforce dark/light theme consistency by auditing all components (navbar, footer, player) against both color schemes; use CSS variables and Tailwind's `dark:` modifier.
  • Keep the UI minimal: remove all metadata badges, source filenames, embed/download/share buttons, and tech-stack footers; retain only player controls, keyboard shortcuts, and a single CTA.
  • Deploy to Vercel with all secrets in Environment Variables; redeploy after adding keys (OpenRouter, Groq, Google Sheets ID, Cloudflare Worker URL).
  • Leverage free tiers aggressively: Cloudflare R2 (10 GB), OpenRouter free models, Groq Whisper API, Google Sheets API, Vercel hobby plan.

Description

Book a call: https://calendly.com/itshassanaziz/discuss-a-project ==== ==== ==== WHAT'S THIS VIDEO ABOUT? Part two of building a self-hosted Loom alternative with AI. I get around transferring my domain to Cloudflare by setting up a Cloudflare Worker to serve files from R2 instead, then clean up the app's design, add a booking CTA, and wire up Google Sheets to track video views, plays, and completions. I also generate auto-chapters using OpenRouter and Whisper transcription, then deploy the whole thing to Vercel with a custom domain. Like, subscribe, & leave me a comment if you have a specific request. Thanks. ==== ==== ==== TIMESTAMPS 00:00 Introduction & Overview 00:37 Cloudflare Worker Workaround 01:04 Binding R2 Bucket to Worker 02:14 Integrating Worker Domain 03:21 Fixing App Design & UI 05:27 Adding Book-a-Call CTA 05:49 Choosing Google Sheets as Database 07:52 Design Fixes Completed 08:08 Setting Up Video Tracking 09:15 Google Cloud & Sheets API Setup 10:00 Managing OAuth Tokens & Secrets 12:26 Fixing Authentication Scope Errors 13:38 Testing Tracking Functionality 14:02 Planning Chapter Generation 14:30 Configuring OpenRouter API 16:20 Architecting the Chapters Workflow 17:29 Adding Transcription for Captions 18:55 Explaining Groq Whisper Setup 20:30 Reviewing Implementation Plan 21:36 Deploying to Vercel 23:16 Switching Models Due to Rate Limits 24:36 Testing Final Chapters Output 25:22 Project Complete & Final Thoughts WATCH THESE NEXT https://www.youtube.com/watch?v=FEQ-Is5yQHk https://www.youtube.com/watch?v=nkXvmzMyc2M https://www.youtube.com/watch?v=y1UWXjqP85w ==== ==== ==== WHY LISTEN TO ME? Hey everyone, I'm Hassan. I run an AI/Automation and Software Development agency at hassandev.me. I've built custom workflows that save clients 20+ hours every week. I've helped businesses solve CRM issues that directly led to an increase of $430k CAD in quotations. I've scaled platforms to 100,000 users and beyond. I also enjoy making content and sharing what I learn with the world. I love to yap on YouTube, as you can see. I'm very active here, and on X, so if you want to reach out to me, leave a comment or DM me on X. MY LINKS Website: https://www.hassandev.me Portfolio: https://www.hassandev.me/work YouTube: https://www.youtube.com/@itshassanaziz?sub_confirmation=1 My Book: https://www.hassandev.me/designing-websites X / Twitter: https://x.com/intent/user?screen_name=nothassanaziz Instagram: https://www.instagram.com/hassansdev/ LinkedIn: https://www.linkedin.com/in/hassan-aziz-web

Transcript

Auto-generated transcript
Hey guys, welcome back. In this video, we're going to continue building our own self-hosted alternative to Lube. Let's get into it. So we made plenty of progress in the last video in part one. This is part two. We got stuck in one of the steps, which is in order to serve videos through Cloudflare or in R2 bucket, we had to set up Cloudflare CDN. And in order to do that, we had to transfer our domain and DNS records and everything to Cloudflare, which is something that I didn't want to do because I'm using Vercel. So I found a workaround and I'm going to show you that workaround in this video. What you can do is you can go over to compute and then workers and pages and you can create a Cloudflare worker over here. I've already done that, but I'm just going to show you the steps so you can do this yourself too. You're just going to click create application, I'm going to start with a hello world template and it'll give you all this basic code. You can just set the name of the worker over here. And once you've done that, just create the worker. All right. And then you'll have a CloudFlare worker that looks something like this. The next thing you're going to have to do is go into bindings, then add binding, go down here to R2 bucket add binding and then you put a variable over here like my bucket or something and you connect your bucket to this and then as for the actual code let me just show you the code over here this is the code that we're using it's just like 70 lines of really basic code and all it does is that it just forwards our requests to the r2 bucket so that if i try to fetch a file from this worker it's going to fetch that same file from the r2 bucket and just return that to me i just use gemini to generate this worker and all you really have to do is just go over to any chat bot ask it to write you a basic cloudflare worker that can fetch files from an r2 bucket just do that add the binding and attach your r2 bucket to that binding and you'll be done and then you can just use your workers url like for instance i have one over here and i can say resume.pdf and it should open it up right there we go so this worker is now fetching files from our r2 bucket and we don't need to use the r2.dev domain which is not intended for production anyway once you've set this up go over to your editor and let's actually integrate this so what i'm going to do is i'm going to copy this domain this entire url paste it over here and then just ask my agent to use that from now on instead of using the r2 domain which is obviously not intended for production use and is rate limited anyway use this worker domain that I set up in Cloudflare All requests to the r2 bucket files should now go through this bucket, this worker. Okay, let's fix this prompt real quick. And there we go. Now, while it does that, let's also start the development server so we can start fixing the design of this app as well, which if you remember is very horrible we're gonna change this uh home page as well a little bit i don't need it to be anything fancy just something minimal because like the home page isn't gonna get used all that much right we're just going to you know upload outreach videos and then just send the link to that exact video to the client so we don't really need a home page for this but we're just gonna have a minimal template that is just different from this default next.js crap right anyway there we go now we're using the actual worker. Now let me go over to a video URL. Let's pick this one and start fixing the design because the design of this entire app is very horrible. So to do this, let's just take some screenshots I want to remove that Wittstack player thing remove the Wittstack player text from the menu bar or actually that called the nav bar There we go What else I want to remove all of this stuff Let's see. Let's take a full screenshot of the entire section over here. Remove the source file. There's no need to show the entire file name. Remove the fingerprint thing you have over here. Also remove the R2 bucket text and the full HD 1080p text over here. No need to show an embed button or an R2 button or a download button or a share video button at all. Remove the entire storage and delivery section as well and remove the brand color spectrum as well. There's no need to show any of these things. You will still keep the player keyboard shortcut section because I think that can be useful but remove everything else. And from the footer, remove the powered by text that tells the user our tech stack. No need to show that. And remove the theme and the custom HSL shade you've written over there as well. Let's transcribe all of that. And let's also fix the issues with our theme, right? So if I go over here, if I turn on light theme, the text just disappears, right? So let's fix that as well. Also, the design is not configured for light theme and dark theme properly. It looks fine in dark theme, but if I turn on light theme, some of the text disappears. and turns completely white or other elements just start looking weird make sure that the design is configured for both light and dark themes both themes need to look really good no matter which one i'm using make sure all elements can dynamically switch between light and dark themes okay now let's send that prompt let's turn on dark mode again so we don't get blinded by this white screen and let's wait for that to finish in the meantime i think we can mark this task complete. Also, I want to add a CTA just below the video. So let's ask it to do that as well. Add a CTA button called book a free call at the bottom of the video player. I'll give you the link to that button over here as well. And then just paste my Calendly link over there. And the agent will take care of that as well. Once it's done fixing the design and everything. By the way, we're also going to need some sort of a database to add this tracking information so we can track how many times a video was played, whether a video was played to completion or it was just, you know, the client played it like halfway through and then just stopped. We want to add tracking for these things and we want some sort of a database to do this, right? Now, I don't want to have a Postgres database for this because then I have to you know go open up my computer run psql or the docker container for this and just run some psql queries and that just extra steps right what i much prefer to do nowadays is to just set up a google sheet where i store all of this data instead not something a programmer does usually but i just prefer this approach so much more so yeah we're gonna set up a database over here i'll just call this loom outreach database and we want to store a few things over here we'll store the url we'll store the page views video plays and video completions there we go it's gonna make this look a little bit better as well because why not okay in the meantime the agent has finished updating our design so let's see what it looks like now let's refresh the page okay not as good as i wanted it to be a light theme looks amazing there's the book of call cta okay i actually like this a lot more but the dark theme still needs work most changes look amazing but the nav bar and the footer in the dark theme does not actually look dark i'm in my dark theme in my os right now and the nav bar and footer are you still using light colors and light backgrounds whereas the rest of the page has correctly updated to the dark mode fix that however in the light theme everything looks perfect so don change anything over there also I gonna create a new file over here in this lib folder called crack in sheet this is where we're going to I going to add all the Google sheets API stuff so we can save all our data and there we go the design has completely fixed itself as well I actually like this design I'm gonna go with it now since all the design stuff has been fixed let's create and let me make sure these errors are actually fixed as well before we do anything else let's refresh the page okay yeah no errors perfect if i switch the team no errors perfect now start a new chat and ask the agent to create all the google sheet database stuff that we need i'm taking a screenshot of the google sheet as well so we can show the agent what the column names are and how to use them right so paste that in over here as well i want you to add a bunch of minimal tracking mechanisms to our video pages. I want to track how many times a video page has been opened. I want to track how many times the video was actually played and I also want to track how many times a video was played to completion, meaning the user didn stop watching halfway or in the middle of the video He actually watched the entire video all the way to the end I want to track these three things and I want to track all of them in my Google Sheet Set up all of the tracking mechanisms and the Google Sheet API stuff that you need in order to make this work Tell me the environment variables that you need from me in order to accomplish this task and I will add them in the .env file myself. Okay, so my transcription service is a bit finicky. Let's fix this typo over here. It called, instead of writing Google, it said Ogle. Funny. Okay, let's send this. Now, to actually make this work, you are going to have to go over to your Google Cloud Console. And you're going to have to create a project over here, you're going to have to enable the Google Sheets API, and then and then grab your OAuth tokens, client IDs and client secrets. I'm not going to go into how to do all of that stuff. I've already done that for previous apps of my own. But it is a pretty straightforward process and you can ask any chatbot to help you guide to help you enable all of these things just go over to chat gbd or gemini or something and tell them you want to create a google cloud project and enable the google sheets api and they'll tell you everything you need to know or you could just ask your agent over here to do it for you once you're done with all of those things you're probably gonna have uh where is it where's my whiteboard a client secret and a top token file both of these are json files and you're gonna have to store them in your code base just make sure they're in your dot git ignore file as well so you don't commit them to source control now i already have my um client secrets and tokens over here in my video editor project in youtube api ls tokens yeah there we go so what i can do is i can just copy these things over to let's go over here let's say cp to copy the file okay there we go so i've added both of them over here what i'm going to do is i'm going to create tokens folder just to organize both of these and i'll move both of them in there and i'll add it to the git ignore file as well let's say credentials tokens and just ignore that entire folder now instead of environment variables we can just use these client secret and token fields and instead of using a service account we can just use my oauth credentials so let me tell the agent to do that i've created a tokens folder in this directory where i have a token.json and a client secret.json file you can use these two files to interact with the google sheets api on my behalf using my oauth tokens no need to set up service accounts and use that and once again i need to change augle to google before youtube bans me or something and then submit that and the agent will modify the implementation plan over here and then we'll you know have a good plan and we can proceed with it now while it does that let's go back over here to my whiteboard and see what we need to do next so we've created a beautiful minimal design for the app okay done we've also added a cta at the bottom of the video player we're gonna skip this step because we don't need the cloudflare cdn anymore because we're using a cloudflare worker which already gives us cloudflare cdn but without using or without transferring our domain to cloudflare which is something i wanted to avoid as much as possible so i'm just gonna consider this one done as well and it looks like my agent has written most of the code let's uh let it do its thing let's refresh the page over here Okay, so that should count as a page view. Okay, so that didn't work because I forgot to configure the Google Sheet ID, so let's do that. The Google Sheet ID is just this long string of jumbled up characters in your URL. Copy that and put that in your ANV file. Okay, I've done that. Now I gonna go back over to right here Actually no we need to the request had insufficient authentication scopes I think we need to reconnect our OAuth to actually enable this I guess it doesn have the correct permissions right now. Let's copy this error and paste it in the agent and ask it how to resolve this. I'm pretty sure we just need to, you know, reconnect our OAuth stuff and whatever. Okay, so it created a new script for us called authsheets. We just need to run this command and we should be able to, you know, reconnect to our Google Sheets. So let's go and run that. I'll just say bun run art sheets. Okay, I'm in the wrong folder. Let's run that again. And I'm going to go ahead and connect my Google account to this and then come back. Okay, I finished connecting the account. Now it gave me this code that I can just copy and write till there, then just paste it over in my terminal and that should work okay there we go we have a success now if i refresh this page again okay so that still doesn't work we need to fix that somehow okay so it turns out i just had to refresh my or restart my development server once i did that this is working now and you can see over here there we go uh the styling needs to be improved a bit but yeah the tracking at least works fine If I play the video it does track that the video has been played what happens if I complete the video it sends another track request okay and the video has been completed perfect so the tracking works flawlessly now the tracking works flawlessly now let's config let's set this task to complete it as well and then we need to add time stamped chapters to each video to do this I'm just going to use Gemma 4 from open router it's free and we can just use this model to generate the timestamps it also supports structured json output which is extremely useful for us we're going to generate a vtt file this is a captions file we're going to upload it to r2 alongside the video file and yeah then we're just going to set our player to use that vtt file for chapters so let's start configuring all of this in order to do all of these things you're going to have to go over to open router.ai sign up for your account then create a personal API key over here and once you've done that you'll have your API key and you can use it to at the very least use all the free models over here and of course if you add a bunch of credits you can use the paid models as well but we don't need that for this purpose we're going to use gamma for 26 B the free version over here so go over to this page copy the model name and then start a new chat in your agent and then let's give it a prompt i want you to use open router and the gamma 426b model to generate a dot vtt file which will contain the captions and the chapters for this video not captions i just want to generate the chapters for the video so let's fix this prompt up as well for some reason i just can't type g when i'm using my transcription service over here use the following model uh fix that as well paste in the model name over here use the following environment variable for my api key okay open router api key and then we also want to you know give it some docs on how this thing actually works so go over to docs over here the way i configure these kinds of like external apis and everything using agents is to just go over to to their docs, find what I'm looking for, which is structured outputs over here, copy the entire page. Most documentation has this button nowadays, especially ones related to AI and LLMs. Copy the entire thing, paste it over here, and just say something like, below I have the docs for the open router API and how to use structured outputs in it Use those docs to write your script There we go And then let think a bit about the architecture of how we going to actually do this So what we want to do is we want to have a file over here in the scripts folder saying something like generate chapters And then we can just pass that file in over here in the context and say something like the generate chapters TypeScript file is where you will write all of this open router code to actually configure all of this. And then how do we want to host this or add this into the let's say the video player and the R2 bucket, what I'm planning is to run the upload to R2 script, which should automatically generate the chapters and the dot VTT file and then upload that alongside the video file with the exact same file name so that we can easily fetch it. So let's add that to the prompt. Once you're done writing the TypeScript script, you should update the upload to R2 shell script to automatically run this script to generate the VTT file and the chapters for each video. And then you should upload that VTT file to the R2 bucket using the exact same file name as the video file. Then you can fetch that VTT file in the video player and add that to the VidStack video player when we go to the video page Now one thing i forgot to add over here is we are also going to need some sort of a transcription service to generate the captions for this whole thing before we can you know um generate the chapters right so this is going to be a pretty massive prompt so i'm just going to set the thinking level to high and in order to generate captions i'm going to use grok not the twitter grok the other grok with the q These guys offer us OpenAI's Whisper large V3 model, which can basically transcribe all of our audio and generate captions for it. So we're going to use that as well. Or at least that's what you should do. But I already have a transcription service, which is what I use for my video editing, which is in my Phantom editor over here. Let me clear all of this. I can just say something like phantom edit transcribe cloud dash dash help. And I can use this command to transcribe any video file. So I'm just going to copy this entire help text and say something like, you're going to need the captions for the video file if you can generate the chapters accurately. In order to get the captions you can use the Phantom Edit Transcribe Cloud command Below is the help text to use this command And there we go Paste that in Maybe format that as a code block as well because why not And just let the agent figure it out after this This is a pretty massive prompt, but that's why we set the thinking level to high, right? Okay, so for some reason, the Grok website is just not loading. So I'm just going to walk you through what you would have done if you didn't have a transcription service like this already set up, right? basically grok is just an ai llm provider and one of the models that they offer is whisper tree and whisper tree is a speech to text model built by open ai right so you can give it some sort of an audio file and it can just transcribe all of it and give you timestamps and captions for the entire thing super useful and grok actually offers this for free which is why i wanted to use them So since you probably don't have a transcription service like this already set up, what you can do is sign up to Grok, grab the API key, right? And then follow the same process we followed for OpenRouter, which is to go over to the Grok docs, go over to speech to text. And I don't know why this is not loading. Grok's website is just really slow right now, for some reason, while I'm happen when I'm recording things, right? If I wasn't recording a video, this would work flawlessly. But when I need it to work, this is what I have to deal with. But yeah, let me just explain the entire thing. You would go over to speech to text over here. You would copy the entire docs, paste that into your agent, and just ask it to write a transcription service for you that can transcribe a video file or audio file. And then you can use that to generate the timestamps, right? Then you pass that in over here to open router, which will then generate the captions or the chapters for you. And that's pretty much it. Okay. So it came up with an implementation plan. Let's try reading this whole thing. API key. Yup. That's the model we want to use. Naming convention. Yup. Everything looks fine. Okay. Use structured outputs. Yes. We want to have structured outputs with chapters normalize and format them to web vtt syntax save the file okay everything is perfect the upload script is now going to compress the video we already did that then it's going to transcribe the video then generate the chapters upload the compressed mp4 and the vtt chapters all of them and yeah looks fine let proceed with this by the way i know i been recording for a very long time but i just trying to finish this project in this video because we literally almost done The only thing we need to do after this step is to just deploy this to Vercel and add a CNAME record so we can easily watch our videos on our own domain So I'm trying to just finish this whole thing in this video. This might get a bit longer. Just bear with me over here. We're almost done. Just one or two steps left. Okay, skip the building process. You don't really need to run this if your development server is working fine. Okay, and to test this out, we should probably upload the freelancer intro all the way again, right? So let's do that. I'm going to exit the development server, and I'm going to upload a video file again by going to scripts, upload to r2.sh, and then I'll pass in a video file path, and then just pass in my freelancer intro again, and this time name it to something like freelancer intro with chapters.mp4. Let's do that. It's obviously going to compress the whole thing again, and then it'll generate the captions and everything. We're just going to wait for this to finish. And while we do that, let's also deploy this to Vercel so that we can start to finish this whole thing I say create a new project The name will just be watch us in dev No need to customize anything This is just a standard Next project And then just let Vercel handle all of the rest then we can just go over to Vercel, open up the project settings and add the environment variables we're going to need over here, because we are going to need most of them. Okay, so I just added all of my variables to the Vercel project. Let's go over to deployments. And let's redeploy this, We can use the existing build cache. I just want to have the variables that I just added in the actual app. Okay, I forgot to set my open router API key. So that sucks. Okay, so I just added the API key. Now let's try to upload all over again. This is kind of annoying because it's going to run every single step all over again. But yeah, you're just going to have to deal with that, I guess. So let's let this whole thing run and see how it works. Okay, so the google servers returned a rate limit error this happens way too often google just can't keep up with the demand so we're just gonna switch to a different provider and a different model let's use nvidia's nemotron 3 ultra this is a free model let's switch to this one pass in the generate chapter script over here and say use this model from now on honestly i don know why i chose to use gamma 4 and Google servers They just they have such horrible rate limits No matter how many times you send a request no matter when you send a request, it's just going to return a rate limit like nine times out of 10. Because like these guys just can't keep up with the, you know, the demand for this stuff. Okay, let's run this whole thing all over again for the third time now and see how it works. And it turns out the grok website is finally loading as well so yeah what you were supposed to do to generate the captions was to just go over to the speech to text documentation page copy the page paste it into your llm along with your api key which you would put in the env file not in the llm chat and just let it write you a transcription service right okay i think this time it actually worked so if you look at the output over here it generated the captions it compressed the video it generated the chapters file and it uploaded everything. So let's go and see if it works. Fingers crossed. I really hope this works now. And let's actually test this on my Vercel app. So this is the URL of the file, or actually no, this is the old file that we were using. So let's go over to my R2 bucket, or actually, instead of that, here's the file that we can just view. Let's copy this whole URL well let's paste that over here and let's go and see what happens okay so the file name is correct freelancer intro with chapters there's the chapters that i wanted to have introduction and key achievements technical consulting business value end-to-end ownership there we go so that all works now thank goodness the theme works as well and there we go yeah and with that we have accomplished this entire project i'm going to mark all of those tasks complete and uh yeah before this video gets even longer because i've been recording for like an hour already i'm gonna end it over here that right there ladies and gentlemen is how i build projects with ai as you can see the whole thing took me like two or three hours to build in total obviously you know the so you can build apps really really quickly with ai you just have to do it the right way right things like you know having a proper tech stack saving your secrets in your environment variables properly having a good idea of what tools and frameworks and apps to use like i use the cloudflare r2 bucket over here because they offer me 10 gb of storage for free i you know use open router for um generating chapters because they offer free models i use grok for transcription because they give me whisper flow for free not whisper flow the open ai whisper model and so yeah as long as you have a good idea of what you're doing you can build pretty much anything and with that i'm I'm going to end the video over there before it gets even longer. Thank you so much for watching if you stuck to the end. And I hope you built this alongside me and learned a thing or two. Peace.

Share this article

All great things started with a conversation

If you've got a cool project or opportunity and you want me to be a part of it, set up a free meeting with me here, and let's talk. 😊