Building CINEFLIX: Lessons From a Next.js 16 Streaming App
What I learned building a Netflix-style OTT app — profiles, trailers, free full-length films and auth — on Next.js 16 with zero video hosting.
CINEFLIX started as a weekend question: how much of a modern streaming experience can you build without hosting a single video file? The answer turned out to be "almost all of it". The app has sign up, profiles, a billboard with an auto-playing trailer, Top 10 rows, hover previews, a detail popup with episodes, My List, search and full-length free films with Continue Watching.
Don't host what you can stream
Video hosting is the expensive, operationally heavy part of any OTT product, so CINEFLIX avoids it entirely. Catalogue data — titles, artwork, cast, seasons and episodes — comes from the TMDB API. Trailers play from YouTube. Full-length free films stream straight from the Internet Archive, which hosts a large library of public-domain cinema.
The app's job becomes orchestration: fetch, cache and present. Nothing is uploaded or stored apart from the user's own data.
Let the server do the data work
Next.js 16 makes it natural to fetch TMDB data in server components, so the API token never reaches the browser and the first paint already contains real rows of titles.
- Keep the TMDB read token server-side only
- Fetch every row in parallel with
Promise.allinstead of one after another - Cache catalogue responses — CINEFLIX revalidates TMDB data hourly, because trending lists don't change per request
Third-party catalogue data is a dependency like any other. Wrap it in one module so every page talks to your API, not theirs.
Profiles are a data-model decision, not a UI one
"Who's watching?" looks like a screen, but it shapes the whole schema. My List, Continue Watching and Kids mode all belong to a profile, not to the account. CINEFLIX allows up to five profiles per account, and every new account starts with a regular profile and a Kids profile. A Kids profile doesn't hide items in the UI — it requests a different set of rows, such as animated and family films capped at a PG certification, and only classic cartoons in the free-films section.
Getting this right early meant every later feature — watch progress, personal lists — simply took a profile_id and worked.
Auth without a framework
Authentication is deliberately small: passwords are hashed with bcrypt, and sessions are signed JWTs using jose, stored in an HTTP-only cookie. There is no auth vendor to configure, and the whole flow fits in a few files I fully understand.
Two databases, one code path
Production uses Neon serverless Postgres. Locally, an embedded PGlite database starts automatically when DATABASE_URL is empty, so a fresh clone runs with just npm install and npm run dev. Tables are created on first use, which removes a whole class of "did you run the migrations?" setup problems.
The details that make it feel real
- 01Hover previews wait about half a second before opening, so sweeping the mouse across a row doesn't trigger a wall of popups
- 02The billboard trailer autoplays muted, with a one-click unmute button
- 03Continue Watching saves progress every ten seconds while playing — and again on pause or when the tab is hidden, using
navigator.sendBeaconso the last position survives closing the page - 04Detail popups fetch episodes one season at a time, only when you open that season
None of these are hard individually. Together they are the difference between a demo and something people actually want to use.
What I'd do next
The next step is server-side recommendations based on watch history rather than TMDB's generic "More Like This", and offline-friendly caching for the catalogue. The architecture already supports both, because the app owns a clean boundary between its data and everyone else's.
You can try the live app or read the source from the CINEFLIX project in my Work section.
Building something that has to scale?
I help teams design architecture, tighten performance and ship products that keep working as the business grows.