DEV Community

Cover image for Chat With Any YouTube Video — My ₹0 RAG App Is Now Live (Here's How the Frontend Works)
Pankaj Batra
Pankaj Batra

Posted on Originally published at pankajbatra.hashnode.dev

Chat With Any YouTube Video — My ₹0 RAG App Is Now Live (Here's How the Frontend Works)

Paste any YouTube link. Chat with the video. Get summaries, study notes, and quizzes — every answer grounded with clickable timestamps that jump the embedded player to the exact moment.

It's live, it's free, and there's no signup: https://youtube-rag-frontend.vercel.app

A week ago I published the backend story — a FastAPI + ChromaDB + Groq RAG pipeline running on ₹0/month, and the three walls I hit shipping it. This post is the second half: the Flutter Web frontend, the interaction pattern that makes the whole thing feel magical, and — because this project apparently refuses to let me ship anything without a fight — three more walls, including my LLM getting shut off two days before launch.

What the finished product does

The flow is deliberately boring: paste a URL, wait a few seconds while the transcript is ingested, and you're in.

From there, four things work end-to-end:

Chat. Ask anything about the video. Answers stream in token-by-token, and every factual claim carries a [ts:MM:SS] citation rendered as a tappable chip. Tap it and the embedded YouTube player seeks to that exact second. The retrieved source chunks appear before the answer even starts generating.

Summaries. One tap generates a short TL;DR plus a detailed point-by-point breakdown. Cached after first generation — instant on revisit.

Notes. Full markdown study notes, rendered in-app and downloadable as a real .md file you can drop into Obsidian or Notion.

Quizzes. Five multiple-choice questions generated from the video, with scoring and explanations — and the explanations carry the same tappable timestamp chips, so a wrong answer links you back to the moment in the video that explains it.

No login. No account. Your library and chat history never leave your browser — more on why in a minute.

The mechanic worth explaining: tappable timestamps

If there's one pattern worth stealing from this project, it's this one. It's what makes people stop and say "wait, that's cool," and it's cheap to build once you see the shape of it.

Step 1 — the LLM emits structured markers in plain text. The chat prompt instructs the model to append [ts:MM:SS] next to any factual claim, using the time ranges of the retrieved chunks. No function calling, no JSON schema for the answer — just an inline convention in the text stream. This matters because the answer is streaming; a structured-output format would force me to wait for the full response before rendering anything.

Step 2 — the frontend parses markers mid-render. In Flutter, the assistant message is built with Text.rich and a list of InlineSpans. A regex scans the accumulated text for [ts:MM:SS] matches; each match becomes a WidgetSpan wrapping a small CitationChip widget, and the surrounding text stays ordinary TextSpans. Because parsing happens on every rebuild, chips materialize live as tokens arrive — you can watch a citation pop into existence mid-sentence.

Step 3 — the chip talks to the player. The YouTube embed runs through youtube_player_iframe, and its controller lives in a Riverpod provider scoped to the current video. A chip tap reads that controller and calls seekTo(seconds) followed by playVideo(). That's the entire integration: a regex, a widget span, and a controller reference.

The same parser is reused inside quiz explanations, which is where the pattern earns its keep twice — getting a question wrong and tapping straight to the moment that explains it is the closest this app gets to a teaching assistant.

The general lesson: when your output is a stream, put structure in the text as lightweight conventions rather than around the text as schemas. Parse leniently on the client. Text and video stop being two panes on a screen and become one linked document.

Why Flutter Web (an honest answer)

The predictable question: why not React?

Because I've written Flutter for four years and I can move fast in it. That's most of the answer, and I think it's a legitimate one — familiarity is a real engineering resource. But there were structural reasons too:

  • One codebase, more targets later. A mobile app is a build target away, not a rewrite. For a video-study tool, a phone app is an obvious future.
  • Riverpod's family providers gave me per-video state isolation for free. Every provider — chat history, summary, notes, quiz, player controller — is keyed by video_id. Switching videos in the sidebar swaps the entire state tree with zero manual cleanup.
  • The architecture transfers. Feature-first clean architecture (data / domain / presentation per feature) is how I structure mobile apps; the six features here (library, chat, summary, notes, quiz, health) follow the same shape.

The honest trade-off: Flutter Web ships a heavier initial bundle than an equivalent React app — several MB — so first load is slower. For a productivity tool people bookmark and return to, I'll take that trade. If this were a landing page or a content site, I wouldn't.

Rest of the frontend stack, briefly: Riverpod with codegen for state, freezed for immutable models, go_router for navigation, dio for HTTP with a hand-rolled SSE parser for the chat stream, sembast_web over IndexedDB for local storage, Vercel free tier for hosting.

The privacy angle: no accounts, by design

Post 1 mentioned the backend is stateless per user. Here's what that actually buys once there's a real product on top:

Your data lives in your browser. The video library and every chat message are stored in IndexedDB (via sembast_web). The backend is a shared cache keyed by YouTube video ID — it knows which videos have been processed, but it has no concept of who you are.

The consequences stack up nicely:

  • Onboarding is instant. Paste a URL, start chatting. No signup wall in front of a tool whose whole pitch is "faster than scrubbing the video yourself."
  • No PII on servers. There is nothing to breach, nothing to subpoena, no GDPR data-subject request to handle, because there is no user data server-side.
  • The shared cache stretches the free tier. When two people study the same video, it's ingested once. Popular videos get cheaper per user, not more expensive.

And the honest trade-off, surfaced right in the app's About dialog: clear your browser data and your library is gone. That's deliberate. Local-first means the user owns the data — including the responsibility for it.

This decision also created the most interesting UX bug of the project, which brings us to the walls.

Wall 4: My LLM was shut off two days before launch

Post 1 readers will remember Wall 2: Google deprecated my embedding model mid-build. I wrote "read release notes, keep model names in env config." I did not expect to be rehearsing that lesson again within weeks.

Mid-frontend-build, every summary, notes, and quiz call started returning LLM_ERROR. Chat broke on new turns. Nothing in my code had changed. The cause: Groq had announced the deprecation of llama-3.3-70b-versatile back in June, and I hit the actual shutoff while testing the integration.

The fix was almost anticlimactic, and that's the point:

  1. Picked Groq's recommended replacement — openai/gpt-oss-120b. Still free tier, same 128K context window, faster inference. Arguably an upgrade.
  2. Updated one environment variable on Render.
  3. Production recovered in about thirty seconds.

The proper cleanup — new defaults in config, updated .env.example, and a RUNBOOK.md so future-me knows the drill — followed as a regular commit, calmly, with production already healthy.

Here's the part worth dwelling on: this is the third provider rug-pull this project has absorbed in a matter of weeks. Gemini killed my embedding model. A hosting option I'd evaluated moved its free tier behind payment. Now Groq retired my LLM. None of these were my bugs; all of them would have been my outages.

In the LLM era, model deprecations arrive with the frequency of OS security patches. The free-tier AI stack is genuinely excellent — this whole product runs on it — but churn is its hidden cost, and the tax you pay is architectural: model names in env vars, provider calls behind your own abstractions, and a runbook for the day the 404 arrives. Not nice-to-haves. Survival gear.

Wall 5: The backend forgot; the browser remembered

This one is a direct consequence of the local-first design, and it's my favorite wall of the six because it's a product problem, not an infrastructure one.

Render's free tier has ephemeral disk: every redeploy wipes ChromaDB and SQLite. The shared cache resets. But the user's library lives in their IndexedDB, which persists just fine. Result: the sidebar cheerfully lists a video the backend has never heard of. Open it, and the summary/notes/quiz tabs spiral into confusing retry loops against a 404.

The state on the client and the state on the server had diverged, and the app had no idea.

The fix is explicit stale-state detection. Opening a video fires a silent existence check (GET /videos/{id}). On a 404, the UI drops a banner:

"This video exists in your local library but is missing from backend cache."

With two actions:

  • Re-sync — reconstructs the YouTube URL from the stored video ID (https://www.youtube.com/watch?v={video_id} — YouTube IDs are deterministic, so I never needed to store the URL at all) and re-ingests through the normal flow.
  • Remove local copy — for videos you're done with.

While stale, chat history stays readable but sending is disabled, and the content tabs show guidance instead of raw errors.

Two deliberate choices in that design. First, no silent auto-re-ingest. The app could quietly re-process every stale video in the background — and burn free-tier transcript quota on work the user never asked for. Asking is cheaper and more honest. Second, the banner names the actual situation instead of a generic "something went wrong." Users can handle the truth that a free-tier cache resets; what they can't handle is a retry spinner with no explanation.

The lesson generalizes to any local-first app with a server cache: ephemeral storage is a design constraint, not a footnote. Detect divergence explicitly. Tell the user. Let them decide.

Wall 6: Vercel couldn't build my app

Flutter Web on Vercel is a slightly odd couple — Vercel has no Flutter runtime, so the SDK gets cloned during the build via an install script. That part worked. What didn't: code generation.

This codebase leans on codegen — freezed for models, riverpod_generator for providers. Running build_runner inside Vercel's constrained build container blew the Dart analyzer's stack. The build hung past fourteen minutes and died with a stack trace long enough to need its own pagination. Twice.

The fix is the standard, mildly controversial pattern: commit the generated files. .freezed.dart and .g.dart files go into git; Vercel skips codegen entirely and just compiles. A regenerate.sh script handles local regeneration — run it after any model or provider change, commit the output.

The trade-off is honest: one extra manual step in the dev loop, in exchange for fast, deterministic CI builds. For a solo project on free-tier CI, that trade isn't close. (A smaller quirk from the same session: Vercel caps the buildCommand string at 256 characters, which my build sequence exceeded — so the whole thing moved into vercel-install.sh / vercel-build.sh scripts anyway. Cleaner, and testable locally with a plain bash vercel-build.sh.)

The lesson is the same one Wall 3 taught from a different angle: your CI environment is not your machine. Different resource limits, different failure modes. Deploy early — the first deploy is a test of the pipeline, not the product.

The bill, updated

Post 1 ended with a seven-line infrastructure bill totalling ₹0. The product now has a frontend, a domain, and real users. The updated bill:

Service Purpose Monthly cost
Render Backend hosting ₹0
Vercel Frontend hosting ₹0
Groq LLM inference (gpt-oss-120b) ₹0 (free tier)
Google Gemini Embeddings ₹0 (free tier)
Supadata YouTube transcripts ₹0 (free tier)
ChromaDB Vector storage ₹0 (local, open source)
SQLite Metadata storage ₹0 (local, open source)
UptimeRobot Keep-alive pings ₹0 (free tier)
Total ₹0

Six walls, two milestones, one full-stack RAG product, zero rupees. The constraint held.

What's next

A few directions I'm weighing, in no particular order: map-reduce summarization for very long videos (the current pipeline favors shorter content), voice input for chat, optional accounts for people who want cross-device sync, and — since the codebase is Flutter — a proper mobile app.

If any of those would be useful to you, say so. That's genuinely how I'll prioritize.

Try it

https://youtube-rag-frontend.vercel.app — paste a video you've been meaning to study, ask it something, tap a timestamp.

It runs on free-tier everything, so be kind: a rare cold start takes a few seconds, and it's a personal project, not a funded product. If it breaks in an interesting way, tell me — interesting breakage is how this entire series got written.

The backend story, including the first three walls, is here: I Deployed a Full RAG Backend for ₹0/Month.


About the Author

I'm Pankaj Batra, a Software Engineer focused on Flutter, automation, and enterprise integrations.

I write about practical engineering: mobile architecture, workflow automation, APIs, event-driven systems, and lessons from production systems.

Connect

If this was useful, follow for more engineering notes.

Top comments (0)