Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

Building a free video downloader and 50+ browser tools on Oracle's free tier

I built SaveAnyReel, a free video downloader (Instagram, TikTok, YouTube, Facebook, Pinterest, Snapchat) with 50+ small tools next to it: PDF, image, audio and calculators. I wanted to run it at $0, so everything lives o

I built SaveAnyReel, a free video downloader (Instagram, TikTok, YouTube, Facebook, Pinterest, Snapchat) with 50+ small tools next to it: PDF, image, audio and calculators. I wanted to run it at $0, so everything lives on Oracle's Always Free ARM VM. Here is how it is put together and what broke.

The stack

  • Next.js 16 (App Router) + React 19 + Tailwind v4 for the site and API routes.
  • Docker Compose on one Always Free ARM VM: Caddy (HTTPS), the Next.js app, a worker, Redis.
  • BullMQ + Redis for the job queues. The API validates a link and enqueues a job; the worker does the heavy lifting.
  • yt-dlp + ffmpeg in the worker. Videos are normalised to H.264 + AAC, because iPhones play VP9/AV1 MP4s as audio only.
  • LibreOffice, poppler, ghostscript, libheif in the worker for the document and HEIC converters.

Keep the server idle: do it in the browser

Most tools never touch the server:

  • PDF merge, split, rotate and JPG to PDF use pdf-lib.
  • Image resize, crop, compress and convert use the Canvas API.
  • QR codes use qrcode; OCR uses tesseract.js (language data is downloaded once, the image never leaves the device).

Only things that really need native tools (Word to PDF, HEIC, video to MP3) go through the queue, with size caps and a per-IP rate limit.

Things that bit me

1. YouTube blocks datacenter IPs. "Sign in to confirm you're not a bot" appeared for every client I tried. The fix that worked for me: a Cloudflare WARP SOCKS5 sidecar container, and only YouTube requests go through it.

2. Do not trust file extensions. My upload check only accepted a certain MP4 header for .m4a. A real user uploaded a WebM/Opus recording saved as .m4a and got "not a real file". I now sniff the content for known media signatures. Plain text (HLS playlists, concat scripts) is always rejected, and ffmpeg gets -protocol_whitelist file, so a crafted file cannot make it fetch URLs.

3. Cache what the API just learned. The app resolves a link, then the worker used to run yt-dlp's slow extraction again. Sharing the metadata through Redis for 10 minutes (plus fetching the video and audio streams in parallel) roughly halved job time: a short YouTube clip went from about 14 s to 6.5 s.

4. Hydration and randomness. A password generator that uses crypto.getRandomValues cannot render on the server. I used useSyncExternalStore with a false server snapshot, so the first client render matches the HTML and the password appears right after hydration.

5. SEO is half the product. Every tool has its own page with FAQ and HowTo schema, a canonical URL and a short guide. I submit URLs with IndexNow so Bing picks up changes quickly.

Honest limits

It only handles public content. Instagram sometimes revokes the logged-in session the extractor needs, so I treat it as flaky and surface clear errors instead of hiding them.

If you want to poke at it: saveanyreel.com. Feedback on any tool is welcome, especially bug reports with the file type that failed.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.