<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pankaj Batra</title>
    <description>The latest articles on DEV Community by Pankaj Batra (@pankaj_batra).</description>
    <link>https://dev.to/pankaj_batra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026737%2F3c166e3d-7c37-4cbd-90f0-281ea0395598.gif</url>
      <title>DEV Community: Pankaj Batra</title>
      <link>https://dev.to/pankaj_batra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pankaj_batra"/>
    <language>en</language>
    <item>
      <title>Chat With Any YouTube Video — My ₹0 RAG App Is Now Live (Here's How the Frontend Works)</title>
      <dc:creator>Pankaj Batra</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:53:26 +0000</pubDate>
      <link>https://dev.to/pankaj_batra/chat-with-any-youtube-video-my-0-rag-app-is-now-live-heres-how-the-frontend-works-4ce7</link>
      <guid>https://dev.to/pankaj_batra/chat-with-any-youtube-video-my-0-rag-app-is-now-live-heres-how-the-frontend-works-4ce7</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It's live, it's free, and there's no signup: &lt;strong&gt;&lt;a href="https://youtube-rag-frontend.vercel.app" rel="noopener noreferrer"&gt;https://youtube-rag-frontend.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A week ago I published &lt;a href="https://pankajbatra.hashnode.dev/i-deployed-a-full-rag-backend-for-0-month-here-are-the-3-walls-i-hit" rel="noopener noreferrer"&gt;the backend story&lt;/a&gt; — 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 &lt;em&gt;more&lt;/em&gt; walls, including my LLM getting shut off two days before launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the finished product does
&lt;/h2&gt;

&lt;p&gt;The flow is deliberately boring: paste a URL, wait a few seconds while the transcript is ingested, and you're in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwse1dfb9q5d12r9r8yez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwse1dfb9q5d12r9r8yez.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, four things work end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat.&lt;/strong&gt; Ask anything about the video. Answers stream in token-by-token, and every factual claim carries a &lt;code&gt;[ts:MM:SS]&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6c295v6mnggfhuzy0xli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6c295v6mnggfhuzy0xli.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7goqgvsse5y8yagrsnk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7goqgvsse5y8yagrsnk.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notes.&lt;/strong&gt; Full markdown study notes, rendered in-app and downloadable as a real &lt;code&gt;.md&lt;/code&gt; file you can drop into Obsidian or Notion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6k3sql8tlbuaswlqeqf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6k3sql8tlbuaswlqeqf.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quizzes.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjlbgfcbc9xrj3ruvb39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjlbgfcbc9xrj3ruvb39.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No login. No account. Your library and chat history never leave your browser — more on why in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanic worth explaining: tappable timestamps
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — the LLM emits structured markers in plain text.&lt;/strong&gt; The chat prompt instructs the model to append &lt;code&gt;[ts:MM:SS]&lt;/code&gt; 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 &lt;em&gt;streaming&lt;/em&gt;; a structured-output format would force me to wait for the full response before rendering anything.&lt;/p&gt;

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

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

&lt;p&gt;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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why Flutter Web (an honest answer)
&lt;/h2&gt;

&lt;p&gt;The predictable question: why not React?&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One codebase, more targets later.&lt;/strong&gt; A mobile app is a build target away, not a rewrite. For a video-study tool, a phone app is an obvious future.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Riverpod's family providers&lt;/strong&gt; gave me per-video state isolation for free. Every provider — chat history, summary, notes, quiz, player controller — is keyed by &lt;code&gt;video_id&lt;/code&gt;. Switching videos in the sidebar swaps the entire state tree with zero manual cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The architecture transfers.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy angle: no accounts, by design
&lt;/h2&gt;

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fca38lhgpvfkpaznwhk49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fca38lhgpvfkpaznwhk49.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The consequences stack up nicely:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This decision also created the most interesting UX bug of the project, which brings us to the walls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 4: My LLM was shut off two days before launch
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;The fix was almost anticlimactic, and that's the point:&lt;/p&gt;

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

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

&lt;p&gt;Here's the part worth dwelling on: &lt;strong&gt;this is the third provider rug-pull this project has absorbed in a matter of weeks.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;churn is its hidden cost&lt;/em&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 5: The backend forgot; the browser remembered
&lt;/h2&gt;

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

&lt;p&gt;Render's free tier has ephemeral disk: every redeploy wipes ChromaDB and SQLite. The shared cache resets. But the user's library lives in &lt;em&gt;their&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;The state on the client and the state on the server had diverged, and the app had no idea.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"This video exists in your local library but is missing from backend cache."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With two actions:&lt;/p&gt;

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

&lt;p&gt;While stale, chat history stays readable but sending is disabled, and the content tabs show guidance instead of raw errors.&lt;/p&gt;

&lt;p&gt;Two deliberate choices in that design. First, &lt;strong&gt;no silent auto-re-ingest.&lt;/strong&gt; 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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Wall 6: Vercel couldn't build my app
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This codebase leans on codegen — freezed for models, riverpod_generator for providers. Running &lt;code&gt;build_runner&lt;/code&gt; 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.&lt;/p&gt;

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

&lt;p&gt;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 &lt;code&gt;buildCommand&lt;/code&gt; string at 256 characters, which my build sequence exceeded — so the whole thing moved into &lt;code&gt;vercel-install.sh&lt;/code&gt; / &lt;code&gt;vercel-build.sh&lt;/code&gt; scripts anyway. Cleaner, and testable locally with a plain &lt;code&gt;bash vercel-build.sh&lt;/code&gt;.)&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The bill, updated
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Backend hosting&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Frontend hosting&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;LLM inference (gpt-oss-120b)&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supadata&lt;/td&gt;
&lt;td&gt;YouTube transcripts&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromaDB&lt;/td&gt;
&lt;td&gt;Vector storage&lt;/td&gt;
&lt;td&gt;₹0 (local, open source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Metadata storage&lt;/td&gt;
&lt;td&gt;₹0 (local, open source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UptimeRobot&lt;/td&gt;
&lt;td&gt;Keep-alive pings&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;₹0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six walls, two milestones, one full-stack RAG product, zero rupees. The constraint held.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;want&lt;/em&gt; cross-device sync, and — since the codebase is Flutter — a proper mobile app.&lt;/p&gt;

&lt;p&gt;If any of those would be useful to you, say so. That's genuinely how I'll prioritize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtube-rag-frontend.vercel.app" rel="noopener noreferrer"&gt;https://youtube-rag-frontend.vercel.app&lt;/a&gt;&lt;/strong&gt; — paste a video you've been meaning to study, ask it something, tap a timestamp.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The backend story, including the first three walls, is here: &lt;a href="https://pankajbatra.hashnode.dev/i-deployed-a-full-rag-backend-for-0-month-here-are-the-3-walls-i-hit" rel="noopener noreferrer"&gt;I Deployed a Full RAG Backend for ₹0/Month&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm Pankaj Batra, a Software Engineer focused on Flutter, automation, and enterprise integrations.&lt;br&gt;&lt;br&gt;
I write about practical engineering: mobile architecture, workflow automation, APIs, event-driven systems, and lessons from production systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://pankajbatra.vercel.app" rel="noopener noreferrer"&gt;https://pankajbatra.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LinkedIn: &lt;a href="https://linkedin.com/in/pankaj-batra-0a294a205" rel="noopener noreferrer"&gt;https://linkedin.com/in/pankaj-batra-0a294a205&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Pankaj0405" rel="noopener noreferrer"&gt;https://github.com/Pankaj0405&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful, follow for more engineering notes.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>rag</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Deployed a Full RAG Backend for ₹0/Month — Here Are the 3 Walls I Hit</title>
      <dc:creator>Pankaj Batra</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:09:34 +0000</pubDate>
      <link>https://dev.to/pankaj_batra/i-deployed-a-full-rag-backend-for-0month-here-are-the-3-walls-i-hit-3nga</link>
      <guid>https://dev.to/pankaj_batra/i-deployed-a-full-rag-backend-for-0month-here-are-the-3-walls-i-hit-3nga</guid>
      <description>&lt;p&gt;I just shipped the backend for &lt;strong&gt;YouTube RAG Chat&lt;/strong&gt;: paste a YouTube link, chat with the video, and get AI-generated summaries, study notes, and quizzes — every answer grounded with clickable timestamp citations.&lt;/p&gt;

&lt;p&gt;It runs in production. It streams responses token-by-token. It costs me exactly &lt;strong&gt;₹0 per month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Getting there was not smooth. My container OOMed on every startup, Google deprecated my embedding model mid-build, and YouTube blocked my server's IP the moment I deployed. This article is the story of the system, the three walls I hit, and what each one taught me about building AI backends under real constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: video is unsearchable
&lt;/h2&gt;

&lt;p&gt;We've all done it: scrubbing back and forth through a 40-minute tutorial trying to find the 90 seconds where the presenter actually explains the thing. Video transcripts exist, but reading a raw transcript is worse than watching the video. What you actually want is to &lt;em&gt;ask the video a question&lt;/em&gt; and jump straight to the answer.&lt;/p&gt;

&lt;p&gt;That's a textbook Retrieval-Augmented Generation problem — and RAG remains the backbone of production AI in 2026, powering the majority of deployed AI applications. So instead of another chatbot-over-PDFs demo, I built RAG over the medium people actually consume: YouTube.&lt;/p&gt;

&lt;p&gt;The requirements I set for myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounded answers.&lt;/strong&gt; Every claim traceable to a moment in the video, via inline timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming.&lt;/strong&gt; First token in 1–2 seconds, not a 15-second spinner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached everything.&lt;/strong&gt; Same video, second user → sub-second responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;₹0 infrastructure.&lt;/strong&gt; Not "cheap." Zero. As a forcing function for good architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last constraint turned out to be the most educational decision of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python 3.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web framework&lt;/td&gt;
&lt;td&gt;FastAPI (async)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORM + migrations&lt;/td&gt;
&lt;td&gt;SQLAlchemy 2 (async) + Alembic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector DB&lt;/td&gt;
&lt;td&gt;ChromaDB (local, persistent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata DB&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Groq — &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt;, streamed via SSE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;Google Gemini — &lt;code&gt;gemini-embedding-001&lt;/code&gt; (768 dims)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcripts&lt;/td&gt;
&lt;td&gt;Supadata API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Render free tier (Docker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep-alive&lt;/td&gt;
&lt;td&gt;UptimeRobot, 10-minute pings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that half of these choices weren't my &lt;em&gt;first&lt;/em&gt; choices. They're what survived contact with production. More on that shortly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Clean architecture, four layers, dependencies pointing strictly inward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routers → Services → Repositories → Infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram description (for Excalidraw):&lt;/em&gt; Four horizontal bands stacked top to bottom. &lt;strong&gt;Band 1 — Routers:&lt;/strong&gt; boxes for &lt;code&gt;/videos&lt;/code&gt;, &lt;code&gt;/chat&lt;/code&gt;, &lt;code&gt;/summary&lt;/code&gt;, &lt;code&gt;/notes&lt;/code&gt;, &lt;code&gt;/quiz&lt;/code&gt;, &lt;code&gt;/health&lt;/code&gt;, each receiving arrows from a "Client" box above. &lt;strong&gt;Band 2 — Services:&lt;/strong&gt; boxes for &lt;code&gt;IngestionService&lt;/code&gt;, &lt;code&gt;ChatService&lt;/code&gt;, &lt;code&gt;ContentService&lt;/code&gt; (summary/notes/quiz). Arrows from each router down to its service. &lt;strong&gt;Band 3 — Repositories:&lt;/strong&gt; &lt;code&gt;VideoRepository&lt;/code&gt;, &lt;code&gt;ChunkRepository&lt;/code&gt;. &lt;strong&gt;Band 4 — Infrastructure:&lt;/strong&gt; boxes for &lt;code&gt;TranscriptFetcher (Supadata)&lt;/code&gt;, &lt;code&gt;EmbeddingClient (Gemini)&lt;/code&gt;, &lt;code&gt;LLMClient (Groq)&lt;/code&gt;, &lt;code&gt;ChromaDB&lt;/code&gt;, &lt;code&gt;SQLite&lt;/code&gt;. Arrows from repositories/services down into infrastructure. One dashed arrow labeled "SSE stream" going from &lt;code&gt;ChatService&lt;/code&gt; back up and out to the Client, annotated with the event sequence &lt;code&gt;sources → tokens → done&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three design decisions worth calling out:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No user identity on the backend
&lt;/h3&gt;

&lt;p&gt;The backend is stateless per user. There are no accounts, no sessions, no per-user tables. Each user's personal video library and chat history will live in their browser's IndexedDB — a frontend concern I'll cover in the next post. The backend only knows about &lt;em&gt;videos&lt;/em&gt;, not &lt;em&gt;people&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This sounds like a limitation. It's actually the unlock for decision #2.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A shared, deduplicated cache
&lt;/h3&gt;

&lt;p&gt;Videos are keyed by YouTube video ID. If User B ingests a video User A already processed, the backend detects the cache hit and returns metadata in under a second — no transcript fetch, no re-chunking, no re-embedding. One popular video costs the system exactly one ingestion, ever.&lt;/p&gt;

&lt;p&gt;Look at the &lt;code&gt;cached: true&lt;/code&gt; flag in the ingestion response below — that request returned in well under a second because someone (me, in this case) had already processed the video.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Timestamps as first-class citizens
&lt;/h3&gt;

&lt;p&gt;The LLM is prompted to emit &lt;code&gt;[ts:MM:SS]&lt;/code&gt; markers inline in its answers, sourced from the retrieved chunks' time ranges. The upcoming frontend will render these as tappable chips that seek the video player. The backend contract is simple: &lt;em&gt;every answer carries its receipts.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The API, live
&lt;/h2&gt;

&lt;p&gt;All examples use &lt;code&gt;{{live}}&lt;/code&gt; as a placeholder for the base URL. Response bodies below are real, pasted from production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ingest a video
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="o"&gt;{{&lt;/span&gt;live&lt;span class="o"&gt;}}&lt;/span&gt;/api/v1/videos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://www.youtube.com/watch?v=xpDnVSmNFX0"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"video_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xpDnVSmNFX0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System Design BASICS: Horizontal vs. Vertical Scaling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gaurav Sen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"thumbnail_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://i.ytimg.com/vi/xpDnVSmNFX0/hqdefault.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;476&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transcript_lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunk_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09T09:20:07.152949Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline behind this: validate the URL → fetch the transcript → chunk it (this 8-minute video became 18 chunks) → embed each chunk via Gemini → store vectors in ChromaDB and metadata in SQLite. On a cache hit, all of that is skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chat, streamed over SSE
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="o"&gt;{{&lt;/span&gt;live&lt;span class="o"&gt;}}&lt;/span&gt;/api/v1/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"video_id": "xpDnVSmNFX0", "question": "What is this video about?", "history": []}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stream arrives as three event types, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;sources&lt;/code&gt;&lt;/strong&gt; — the retrieved chunks, sent &lt;em&gt;first&lt;/em&gt; so a client can render citations before the answer starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"start_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"end_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;26.829&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This video is on the basics of system design. If you have never designed a system before, this is probably the place..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"start_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;457.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"end_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;475.52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"We design a system which is going to meet the requirements, and the requirements are such that it's going to be Compu..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;token&lt;/code&gt;&lt;/strong&gt; — the answer, one token at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token → {"content":"This"}
token → {"content":" video"}
token → {"content":" is"}
token → {"content":" about"}
token → {"content":" the"}
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;done&lt;/code&gt;&lt;/strong&gt; — stream complete.&lt;/p&gt;

&lt;p&gt;Sending sources before tokens is a small ordering decision with a big UX payoff: the user sees &lt;em&gt;where the answer will come from&lt;/em&gt; while the answer is still being generated. First-token latency in production is 1–2 seconds, courtesy of Groq's inference speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generated content: summary, notes, quiz
&lt;/h3&gt;

&lt;p&gt;Three more endpoints turn any ingested video into study material, each cached after first generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="o"&gt;{{&lt;/span&gt;live&lt;span class="o"&gt;}}&lt;/span&gt;/api/v1/videos/xpDnVSmNFX0/summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"video_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xpDnVSmNFX0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"short"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This video covers the basics of system design, starting with a simple algorithm running on a computer and exposing it to others through an API..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"detailed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"The video starts by introducing the concept of system design and how it begins with a simple algorithm running on a computer."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"As the algorithm becomes useful to others, it needs to be exposed through an API, allowing others to send requests and receive responses."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09T09:45:48.459477Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quiz endpoint is the most interesting of the three. The LLM must return strict, structured JSON — question, four options, correct index, explanation with a timestamp — and the backend validates the structure before accepting it (agent-style output validation, retry on malformed responses):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="o"&gt;{{&lt;/span&gt;live&lt;span class="o"&gt;}}&lt;/span&gt;/api/v1/videos/xpDnVSmNFX0/quiz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"video_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xpDnVSmNFX0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"question_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is the purpose of exposing code using an API?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"To store output in a file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"To connect to a database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"To allow others to use the code over the internet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"To configure endpoints"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"correct_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The purpose of exposing code using an API is to allow others to use the code over the internet, as stated at [ts:02:45]."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is the difference between a desktop and a cloud?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"A desktop is a set of computers, while a cloud is a single computer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"A desktop is a single computer, while a cloud is a set of computers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"A desktop is used for personal use, while a cloud is used for business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"A desktop is faster than a cloud"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"correct_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A desktop is a single computer, while a cloud is a set of computers that can be used to run a service, as stated at [ts:06:30]."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full endpoint list: &lt;code&gt;POST /videos&lt;/code&gt;, &lt;code&gt;GET /videos/{id}&lt;/code&gt;, &lt;code&gt;POST /chat&lt;/code&gt;, &lt;code&gt;POST /videos/{id}/summary&lt;/code&gt;, &lt;code&gt;POST /videos/{id}/notes&lt;/code&gt;, &lt;code&gt;POST /videos/{id}/quiz&lt;/code&gt;, &lt;code&gt;GET /health&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now for the part that actually earned this article: nothing above worked on the first deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 1: The 512 MB ceiling
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This is the wall that reshaped the whole architecture, so it gets the deepest treatment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My original embedding plan was the obvious one: run &lt;code&gt;sentence-transformers&lt;/code&gt; locally with &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;. It's the default in every RAG tutorial, it's free forever, and it works beautifully on a dev machine.&lt;/p&gt;

&lt;p&gt;Then I deployed to Render's free tier and the container OOMed on startup. Every time.&lt;/p&gt;

&lt;p&gt;The math, once I actually did it, was brutal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PyTorch runtime: several hundred MB of RAM before doing anything useful&lt;/li&gt;
&lt;li&gt;The MiniLM model loaded into memory&lt;/li&gt;
&lt;li&gt;ChromaDB's own footprint&lt;/li&gt;
&lt;li&gt;FastAPI + the async stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: ~800 MB. Render free tier: 512 MB.&lt;/strong&gt; The container never even reached the health check. There was no clever flag or lazy-loading trick that closes a 300 MB gap — the architecture itself was wrong for the box.&lt;/p&gt;

&lt;p&gt;So I stopped trying to squeeze a local model into 512 MB and asked a better question: &lt;em&gt;why is my 0.1 vCPU web server doing ML inference at all?&lt;/em&gt; Embedding is a compute-heavy, stateless operation — exactly the kind of thing to push to a managed API. I migrated embeddings to Google Gemini's embedding API.&lt;/p&gt;

&lt;p&gt;The results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend RAM: ~800 MB → ~200 MB.&lt;/strong&gt; Comfortable headroom on a 512 MB box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker image: shrank by ~500 MB.&lt;/strong&gt; No PyTorch, no model weights. Final compressed image is under 500 MB — small for an AI backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding quality went &lt;em&gt;up&lt;/em&gt;.&lt;/strong&gt; 768 dimensions vs MiniLM's 384, with roughly a 10-point improvement on MTEB benchmarks. The constraint didn't just save the deploy — it produced better retrieval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; free-tier constraints are an architecture review you didn't ask for. Local models are lovely on a beefy dev machine and terrible on a 0.1 vCPU box. If a component is stateless and compute-heavy, it probably doesn't belong in your web server's process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 2: My embedding model no longer existed
&lt;/h2&gt;

&lt;p&gt;Fresh off the Wall 1 fix, I wired up Gemini's &lt;code&gt;text-embedding-004&lt;/code&gt; — the model most Gemini RAG tutorials reference — and the very first call returned a 404.&lt;/p&gt;

&lt;p&gt;Not a rate limit. Not an auth error. The model was &lt;em&gt;gone&lt;/em&gt;. Google had shut down &lt;code&gt;text-embedding-004&lt;/code&gt; on January 14, 2026, and I'd picked my model from tutorials instead of release notes.&lt;/p&gt;

&lt;p&gt;The fix took minutes, which is the actual point of the story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switched to &lt;strong&gt;&lt;code&gt;gemini-embedding-001&lt;/code&gt;&lt;/strong&gt; — stable, generally available.&lt;/li&gt;
&lt;li&gt;It uses &lt;strong&gt;Matryoshka Representation Learning&lt;/strong&gt;, meaning the model is trained so that truncated prefixes of its embedding vector are themselves valid embeddings. The default output is 3072 dimensions; I explicitly request 768, which is plenty for transcript chunks and keeps ChromaDB's storage and query costs down.&lt;/li&gt;
&lt;li&gt;Because the model name lived in an environment variable rather than being hardcoded, the swap was a &lt;strong&gt;one-line config change&lt;/strong&gt;. No code touched, no redeploy logic, no migration script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when you pick an API model, read the release notes and the deprecation timeline, not just the tutorial. And treat model names as configuration, not code — the day your provider kills a model (and that day will come), you'll fix it in one line instead of one afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 3: "Works on my machine" — literally
&lt;/h2&gt;

&lt;p&gt;With memory fixed and embeddings live, ingestion worked flawlessly on my laptop. I deployed. Transcript fetching failed instantly.&lt;/p&gt;

&lt;p&gt;The culprit: &lt;strong&gt;YouTube blocks datacenter IPs.&lt;/strong&gt; My laptop's residential IP fetched transcripts happily via &lt;code&gt;youtube-transcript-api&lt;/code&gt;; Render's cloud IP got stonewalled. This isn't a Render problem — AWS, GCP, Azure all hit the same wall, because YouTube treats datacenter IP ranges as scraper traffic.&lt;/p&gt;

&lt;p&gt;The realistic options were: run my own residential proxy layer (cost, complexity, ToS gray zones), or use a managed transcript API that handles that layer for me. I swapped to &lt;strong&gt;Supadata's API&lt;/strong&gt;, which does exactly that, transparently, with a free tier generous enough for an MVP.&lt;/p&gt;

&lt;p&gt;Two design decisions made this migration painless:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The abstraction held.&lt;/strong&gt; All transcript fetching went through a single &lt;code&gt;TranscriptFetcher&lt;/code&gt; class. Swapping &lt;code&gt;youtube-transcript-api&lt;/code&gt; for Supadata was a single-file change — routers, services, and the chunking pipeline never knew anything happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication multiplies the free tier.&lt;/strong&gt; Because videos are cached by ID, one popular video costs one transcript fetch &lt;em&gt;total&lt;/em&gt; — not one per user. The cache isn't just a latency feature; it's what makes the ₹0 economics hold as usage grows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; "it works on my machine" is a real infrastructure signal, not just a meme. Your laptop and your server can live in genuinely different networks with different rules. And the boring advice about wrapping external dependencies in your own abstraction pays for itself the first time an external dependency betrays you — which, per Walls 2 and 3, was twice in one project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ₹0 bill
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Backend hosting&lt;/td&gt;
&lt;td&gt;₹0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;LLM inference&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supadata&lt;/td&gt;
&lt;td&gt;YouTube transcripts&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromaDB&lt;/td&gt;
&lt;td&gt;Vector storage&lt;/td&gt;
&lt;td&gt;₹0 (local, open source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Metadata storage&lt;/td&gt;
&lt;td&gt;₹0 (local, open source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UptimeRobot&lt;/td&gt;
&lt;td&gt;Keep-alive pings&lt;/td&gt;
&lt;td&gt;₹0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;₹0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The UptimeRobot row deserves a sentence: Render's free tier spins containers down when idle, which means multi-second cold starts for the first unlucky user. A ping every 10 minutes keeps the container warm. Cold starts still exist in theory but are rare in practice (~10 seconds when they happen).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;The numbers, in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend RAM:&lt;/strong&gt; ~200 MB (down from ~800 MB in the original design)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker image:&lt;/strong&gt; under 500 MB compressed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-token latency (chat):&lt;/strong&gt; ~1–2 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-hit responses:&lt;/strong&gt; under 1 second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts:&lt;/strong&gt; ~10 seconds, rare thanks to keep-alive pings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not bad for a stack whose total infrastructure line item is a hard zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The backend streams grounded, timestamped answers — but right now the only client is cURL. Next milestone: a &lt;strong&gt;Flutter Web app&lt;/strong&gt; that turns those &lt;code&gt;[ts:MM:SS]&lt;/code&gt; markers into tappable chips that seek the actual video player, with each user's library and chat history living entirely in their browser. That gets its own post.&lt;/p&gt;

&lt;p&gt;If there's one thing to take from this build: &lt;strong&gt;constraints are a feature.&lt;/strong&gt; The 512 MB ceiling forced a cleaner architecture. The deprecated model enforced config discipline. The IP blocking validated the abstraction layer. A bigger budget would have let me ship all three mistakes to production and never learn from them.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm Pankaj Batra, a Software Engineer focused on Flutter, automation, and enterprise integrations.&lt;br&gt;&lt;br&gt;
I write about practical engineering: mobile architecture, workflow automation, APIs, event-driven systems, and lessons from production systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://pankajbatra.vercel.app" rel="noopener noreferrer"&gt;https://pankajbatra.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LinkedIn: &lt;a href="https://linkedin.com/in/pankaj-batra-0a294a205" rel="noopener noreferrer"&gt;https://linkedin.com/in/pankaj-batra-0a294a205&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Pankaj0405" rel="noopener noreferrer"&gt;https://github.com/Pankaj0405&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful, follow for more engineering notes.&lt;/p&gt;




</description>
      <category>rag</category>
      <category>python</category>
      <category>fastapi</category>
      <category>ai</category>
    </item>
    <item>
      <title>Kafka for Engineers Who've Only Used REST: What Actually Changes</title>
      <dc:creator>Pankaj Batra</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:04:53 +0000</pubDate>
      <link>https://dev.to/pankaj_batra/kafka-for-engineers-whove-only-used-rest-what-actually-changes-32lb</link>
      <guid>https://dev.to/pankaj_batra/kafka-for-engineers-whove-only-used-rest-what-actually-changes-32lb</guid>
      <description>&lt;p&gt;When I first read that Kafka was a "distributed event streaming platform," I nodded, closed the tab, and went back to writing REST endpoints.&lt;/p&gt;

&lt;p&gt;It took me a while to realize the problem wasn't Kafka. It was that every explanation started with &lt;em&gt;what Kafka is&lt;/em&gt; and none of them started with &lt;em&gt;why it exists&lt;/em&gt;. If you already think about systems the way REST teaches you to, most Kafka tutorials feel like they're answering a question you didn't ask.&lt;/p&gt;

&lt;p&gt;This post is for engineers who've built REST APIs for years and want to understand Kafka the way it actually clicks — not the API, not the config, but the shift in what a system is supposed to be.&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;REST is about asking. Kafka is about telling.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that lands, everything else about Kafka stops feeling arbitrary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The REST mental model has a hidden assumption
&lt;/h2&gt;

&lt;p&gt;Every REST system assumes one thing that's so obvious it's almost invisible: &lt;strong&gt;the caller knows who to ask.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your mobile app knows to call the orders API. The orders API knows to call the payments API. The payments API knows to call the fraud service. Someone, somewhere, decided who talks to whom, and that decision is baked into the code.&lt;/p&gt;

&lt;p&gt;Consider an order being placed. In a typical REST setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The order service saves the order&lt;/li&gt;
&lt;li&gt;Then it calls the inventory service to decrement stock&lt;/li&gt;
&lt;li&gt;Then it calls the email service to send a confirmation&lt;/li&gt;
&lt;li&gt;Then it calls the analytics service to log the event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four services. Three couplings the order service has to know about. And when the business decides to add loyalty points on every order, you go back and edit the order service — again.&lt;/p&gt;

&lt;p&gt;You didn't do anything wrong. This is what REST tells you to do. But look at what's really happening: the order service is responsible for knowing every other service that cares about orders. That responsibility grows every time the business grows.&lt;/p&gt;

&lt;p&gt;That's the hidden assumption. In REST, &lt;em&gt;the sender is responsible for knowing all the receivers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kafka actually changes
&lt;/h2&gt;

&lt;p&gt;Kafka flips the direction.&lt;/p&gt;

&lt;p&gt;The order service no longer calls anyone. It writes a single fact to a log:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"order 1234 was placed."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then it's done.&lt;/p&gt;

&lt;p&gt;Inventory, email, analytics, loyalty — none of them are called. They read the log on their own schedule, notice a new order, and each one does whatever it does. The order service doesn't know they exist. It doesn't care.&lt;/p&gt;

&lt;p&gt;That's the whole idea.&lt;/p&gt;

&lt;p&gt;Everything else you'll ever read about Kafka — partitions, offsets, consumer groups, brokers, retention — exists to make that one shift work at scale.&lt;/p&gt;

&lt;p&gt;If you take nothing else from this post, take this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In REST, services &lt;strong&gt;call each other&lt;/strong&gt;. In Kafka, services &lt;strong&gt;publish facts and read facts&lt;/strong&gt;, and no one is called by name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Five things that stop feeling weird once the shift lands
&lt;/h2&gt;

&lt;p&gt;Once you accept the model, a bunch of things that look strange in Kafka tutorials suddenly make sense. Here are the five that tripped me up longest.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Kafka doesn't delete messages after they're read
&lt;/h3&gt;

&lt;p&gt;In a REST world, a response is consumed. Once your client got the JSON back, the server moves on. Message queues work similarly — pop a message off, process it, gone.&lt;/p&gt;

&lt;p&gt;Kafka doesn't do that. Messages sit in the log for as long as you tell it to keep them — days, weeks, forever if you want. The reason is baked into the model: a &lt;em&gt;fact&lt;/em&gt; doesn't stop being true just because one service noticed it.&lt;/p&gt;

&lt;p&gt;If analytics needs to reprocess the last week of orders, the events are still there. If you spin up a new service tomorrow that also cares about orders, it can read every order that ever happened.&lt;/p&gt;

&lt;p&gt;The log isn't a queue. It's a record of everything that occurred, and consumers get to decide when and how often they read from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Consumers track their own position
&lt;/h3&gt;

&lt;p&gt;In REST, the server knows who called it. Requests are tracked, sessions exist, the server is the coordinator.&lt;/p&gt;

&lt;p&gt;In Kafka, the log has no idea who's reading. Each consumer is responsible for remembering where it is in the log — its &lt;em&gt;offset&lt;/em&gt;. If a consumer crashes and restarts, it reads its last saved offset and picks up from there. If you want a second copy of a service to also process every event, it just starts from offset zero and reads independently.&lt;/p&gt;

&lt;p&gt;This is why replay is trivial in Kafka and impossible in REST. Rewinding a REST call doesn't mean anything. Rewinding a Kafka offset means "read those events again."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. You scale by adding partitions, not by adding servers per endpoint
&lt;/h3&gt;

&lt;p&gt;REST scales by putting a load balancer in front of your service and adding more instances behind it. Each request goes to whichever instance is free.&lt;/p&gt;

&lt;p&gt;Kafka scales by splitting the log itself. A topic isn't a single log — it's split into &lt;em&gt;partitions&lt;/em&gt;, and consumers coordinate to divide those partitions among themselves. If you have four partitions and four consumers in a group, each consumer handles one partition. Add a fifth consumer and one of them will sit idle, because a partition is only ever read by one consumer in a group at a time.&lt;/p&gt;

&lt;p&gt;This is a completely different scaling model, and it means the number of partitions you choose is a design decision that affects throughput for the life of the topic. It's not something you tune later without pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Order is only guaranteed within a partition
&lt;/h3&gt;

&lt;p&gt;REST doesn't really care about order. Each request is independent, and if you need ordering, you build it in the application layer.&lt;/p&gt;

&lt;p&gt;Kafka guarantees order — but only within a single partition. Across partitions, all bets are off. This means partitioning is a design choice you make based on what needs to stay ordered.&lt;/p&gt;

&lt;p&gt;If you partition orders by customer ID, all events for one customer stay in order. If you partition by timestamp or randomly, you'll see events for the same customer arrive out of sequence. The moment you choose a partition key, you've decided what "in order" means for that topic.&lt;/p&gt;

&lt;p&gt;Get this wrong and you'll ship a system where events arrive in an order that makes no sense to a human, and debugging it will feel like a nightmare until you notice what's happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. There's no "the server returned an error"
&lt;/h3&gt;

&lt;p&gt;In REST, errors have a shape. The server returns a 500, the client sees it, retries or gives up. Error handling lives in the request path.&lt;/p&gt;

&lt;p&gt;In Kafka, there's no server responding to your call. There's a log. If a consumer fails to process a message, the failure happens on the &lt;em&gt;consumer side&lt;/em&gt;, and the consumer has to decide what to do about it: retry, skip, log, or move the message to a dead-letter topic for someone to look at later.&lt;/p&gt;

&lt;p&gt;This moves error handling out of the request path entirely. You stop asking "what should I return when this fails?" and start asking "what should this consumer do when it can't process what it just read?" That's a different question, and it takes a while to get comfortable with.&lt;/p&gt;

&lt;h2&gt;
  
  
  When REST is still the right answer
&lt;/h2&gt;

&lt;p&gt;None of this makes REST obsolete. It's not supposed to.&lt;/p&gt;

&lt;p&gt;REST is still the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The caller needs an answer &lt;em&gt;now&lt;/em&gt; — an authorization check, a price lookup, a validation&lt;/li&gt;
&lt;li&gt;You're doing CRUD on a single resource&lt;/li&gt;
&lt;li&gt;The integration is between two systems, not many&lt;/li&gt;
&lt;li&gt;The caller genuinely does know who to ask, and that's not going to change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka is the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple systems care about the same event&lt;/li&gt;
&lt;li&gt;You want to be able to add new consumers later without touching the producer&lt;/li&gt;
&lt;li&gt;Events have value beyond the moment they happen — audit, replay, analytics, machine learning&lt;/li&gt;
&lt;li&gt;The coupling between services is starting to feel like the thing you spend most of your time maintaining&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most honest tell is this: &lt;strong&gt;if every new feature means adding another HTTP call from an existing service, you're probably ready for events.&lt;/strong&gt; That pattern doesn't scale — not technically, but organizationally. Every new integration means editing an old service, which means testing it, which means owning it.&lt;/p&gt;

&lt;p&gt;Events break that cycle. New consumer, no producer changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift, restated
&lt;/h2&gt;

&lt;p&gt;Kafka doesn't replace REST. What it replaces is a specific habit — the habit of every service being responsible for knowing who to notify when something happens.&lt;/p&gt;

&lt;p&gt;Once producers only publish facts, and consumers only read what they care about, systems get easier to change. Not easier to build the first time — Kafka has real operational cost, and the mental model takes effort. But easier to &lt;em&gt;change&lt;/em&gt;, which is what actually matters after month three of any real system.&lt;/p&gt;

&lt;p&gt;The hardest part of learning Kafka isn't the API. It's letting go of the assumption that someone always has to be waiting for the answer.&lt;/p&gt;

&lt;p&gt;Once you accept that services can just publish facts into the world and stop caring who reads them, the rest of Kafka is just plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Pankaj Batra&lt;/strong&gt;, a Software Engineer focused on Flutter, automation, and enterprise integrations.&lt;/p&gt;

&lt;p&gt;I write about practical engineering: mobile architecture, workflow automation, APIs, event-driven systems, and lessons from production systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://pankajbatra.vercel.app" rel="noopener noreferrer"&gt;https://pankajbatra.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LinkedIn: &lt;a href="https://linkedin.com/in/pankaj-batra-0a294a205" rel="noopener noreferrer"&gt;https://linkedin.com/in/pankaj-batra-0a294a205&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Pankaj0405" rel="noopener noreferrer"&gt;https://github.com/Pankaj0405&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful, follow for more engineering notes.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>architecture</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Server-Driven Survey Engine in Flutter Without a WebView</title>
      <dc:creator>Pankaj Batra</dc:creator>
      <pubDate>Tue, 21 Jul 2026 12:29:44 +0000</pubDate>
      <link>https://dev.to/pankaj_batra/building-a-server-driven-survey-engine-in-flutter-without-a-webview-4ch9</link>
      <guid>https://dev.to/pankaj_batra/building-a-server-driven-survey-engine-in-flutter-without-a-webview-4ch9</guid>
      <description>&lt;p&gt;This article was originally published on my Hashnode blog and is shared here for the Dev.to community.&lt;br&gt;
Enterprise apps often need forms that change without an app-store release.&lt;/p&gt;

&lt;p&gt;New questions appear. Visibility rules change. Validation logic evolves. Dropdown options come from remote services. Nested sections grow and shrink.&lt;/p&gt;

&lt;p&gt;If every one of those rules lives only in Flutter code, the mobile app becomes a second form engine — expensive to build and easy to drift from the web.&lt;/p&gt;

&lt;p&gt;This post is about a different approach: &lt;strong&gt;server-driven surveys rendered with native Flutter widgets&lt;/strong&gt;, while the &lt;strong&gt;form logic stays outside the UI layer&lt;/strong&gt; — and without wrapping everything in a WebView.&lt;/p&gt;

&lt;p&gt;I won’t walk through proprietary implementation details. I’ll share the &lt;strong&gt;design thinking&lt;/strong&gt;, the &lt;strong&gt;trade-offs&lt;/strong&gt;, and the &lt;strong&gt;lessons&lt;/strong&gt; that mattered most.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Challenge Isn’t UI
&lt;/h2&gt;

&lt;p&gt;Drawing text fields and dropdowns is the easy part.&lt;/p&gt;

&lt;p&gt;The hard part is keeping behavior consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conditional visibility and enablement&lt;/li&gt;
&lt;li&gt;Validation and error messaging&lt;/li&gt;
&lt;li&gt;Calculated values and expressions&lt;/li&gt;
&lt;li&gt;Multi-step navigation&lt;/li&gt;
&lt;li&gt;Nested / repeating sections&lt;/li&gt;
&lt;li&gt;Remote option lists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the web, mature form libraries already solve much of this.&lt;/p&gt;

&lt;p&gt;On mobile, teams often choose one of three paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild the logic in Dart&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed a WebView&lt;/strong&gt; and reuse the web form
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid model&lt;/strong&gt; — native UI, shared logic engine&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We chose the third.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Avoid a WebView?
&lt;/h2&gt;

&lt;p&gt;WebViews are fast to ship, but they come with costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavier memory and less “native” feel&lt;/li&gt;
&lt;li&gt;Awkward integration with camera, files, signatures, and platform UX&lt;/li&gt;
&lt;li&gt;Harder theming consistency with the rest of the app&lt;/li&gt;
&lt;li&gt;Debugging that feels like maintaining two apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production mobile UX, native widgets are usually worth the extra design work.&lt;/p&gt;

&lt;p&gt;The constraint we accepted:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The UI can be Flutter-native.&lt;br&gt;&lt;br&gt;
The &lt;strong&gt;meaning&lt;/strong&gt; of the form (rules, state, validity) should not be reinvented in Flutter.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Design Principle That Unlocked Everything
&lt;/h2&gt;

&lt;p&gt;The most important decision was ownership:&lt;/p&gt;

&lt;h3&gt;
  
  
  One layer owns form logic. Flutter owns presentation and device I/O.
&lt;/h3&gt;

&lt;p&gt;That means Flutter should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render what the logic layer says is currently visible&lt;/li&gt;
&lt;li&gt;Capture user input&lt;/li&gt;
&lt;li&gt;Handle device features (files, camera, signature, etc.)&lt;/li&gt;
&lt;li&gt;Apply theming and accessibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flutter should &lt;strong&gt;not&lt;/strong&gt; become the place where business rules are reimplemented “just for mobile.”&lt;/p&gt;

&lt;p&gt;This single rule prevents a class of bugs where mobile and web disagree about whether a field should appear, whether a value is valid, or what an expression means.&lt;/p&gt;




&lt;h2&gt;
  
  
  Server-Driven UI, Practically
&lt;/h2&gt;

&lt;p&gt;In this model, the app receives a form definition (JSON) from the backend.&lt;/p&gt;

&lt;p&gt;That definition describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structure (pages, panels, questions)&lt;/li&gt;
&lt;li&gt;Behavior (conditions, validators, expressions)&lt;/li&gt;
&lt;li&gt;Presentation hints (titles, layout flags, locales)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mobile client’s job is to &lt;strong&gt;interpret and render&lt;/strong&gt;, not to hardcode each form.&lt;/p&gt;

&lt;p&gt;That’s what “server-driven” means here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product can iterate forms without waiting for a Flutter release for every schema change&lt;/li&gt;
&lt;li&gt;Mobile stays aligned with the same logic model used elsewhere&lt;/li&gt;
&lt;li&gt;Engineering effort shifts from “build every form” to “build a reliable renderer”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Made This Hard (Without the Internals)
&lt;/h2&gt;

&lt;p&gt;Hybrid systems fail in the seams — the places where two runtimes meet.&lt;/p&gt;

&lt;p&gt;A few categories of problems showed up repeatedly:&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Nested structures break naive assumptions
&lt;/h3&gt;

&lt;p&gt;Top-level questions behave differently from questions nested inside repeating or dynamic containers.&lt;/p&gt;

&lt;p&gt;If your mental model is “look up a field by name and update it,” nested forms will eventually surprise you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; design for ownership and context early, not after the first nested bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Presentation flags are easy to misread
&lt;/h3&gt;

&lt;p&gt;A flag that hides a &lt;strong&gt;panel title&lt;/strong&gt; is not the same as hiding every child title.&lt;/p&gt;

&lt;p&gt;Some question types also have different title semantics than ordinary inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; match the upstream form engine’s meaning before inventing mobile-only rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Expression language has a strict shape
&lt;/h3&gt;

&lt;p&gt;Operators and functions are not interchangeable.&lt;/p&gt;

&lt;p&gt;A valid expression can look “almost right” and still fail because the syntax is interpreted differently than expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when logic “doesn’t work,” verify expression form before assuming the mobile layer is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Performance is a product of strategy, not just widgets
&lt;/h3&gt;

&lt;p&gt;Large forms stress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;startup cost&lt;/li&gt;
&lt;li&gt;communication between UI and logic layers&lt;/li&gt;
&lt;li&gt;remote option loading on mobile networks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fetching everything up front can feel simple and then become slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; measure separately — init cost, interaction cost, and network cost are different problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs We Accepted
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you gain
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Native Flutter UX&lt;/li&gt;
&lt;li&gt;Stronger parity with an existing form logic ecosystem&lt;/li&gt;
&lt;li&gt;Faster iteration on form content from the server&lt;/li&gt;
&lt;li&gt;Cleaner separation between UI and rules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you pay
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bridge complexity between runtimes&lt;/li&gt;
&lt;li&gt;More careful debugging across layers&lt;/li&gt;
&lt;li&gt;Constraints around what the logic runtime can do directly (especially without browser APIs)&lt;/li&gt;
&lt;li&gt;Ongoing discipline so Flutter doesn’t quietly reabsorb business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture is not “simpler.”&lt;br&gt;&lt;br&gt;
It is &lt;strong&gt;clearer&lt;/strong&gt; — if the ownership boundaries stay honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Principles I’d Reuse on the Next Project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose a source of truth for form logic and protect it.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer native rendering when UX quality matters.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat nested forms as a first-class design problem.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t optimize everything on day one — instrument first.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce edge cases in demos&lt;/strong&gt; (nested sections, remote options, expression rules).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document decisions, not just features&lt;/strong&gt; — future you will need the “why.”&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building a server-driven survey experience in Flutter is less about clever widgets and more about &lt;strong&gt;boundary design&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Flutter owns too much logic, you rebuild a form engine.&lt;br&gt;&lt;br&gt;
If you lean only on WebView, you sacrifice native quality.&lt;br&gt;&lt;br&gt;
If you split responsibilities cleanly, you get something harder to build — and much easier to evolve.&lt;/p&gt;

&lt;p&gt;The goal isn’t to expose every internal mechanism publicly.&lt;/p&gt;

&lt;p&gt;The goal is to ship forms that stay correct as the business changes — without turning every schema update into a mobile release.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Pankaj Batra&lt;/strong&gt;, a Software Engineer focused on Flutter, automation, and enterprise integrations.&lt;/p&gt;

&lt;p&gt;I write about practical engineering: mobile architecture, workflow automation, APIs, and lessons from production systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://pankajbatra.vercel.app" rel="noopener noreferrer"&gt;https://pankajbatra.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LinkedIn: &lt;a href="https://linkedin.com/in/pankaj-batra-0a294a205" rel="noopener noreferrer"&gt;https://linkedin.com/in/pankaj-batra-0a294a205&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Pankaj0405" rel="noopener noreferrer"&gt;https://github.com/Pankaj0405&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful, follow for more engineering notes.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
    <item>
      <title>5 Power Automate Design Mistakes That Slow Down Your Flows (And How to Fix Them)</title>
      <dc:creator>Pankaj Batra</dc:creator>
      <pubDate>Mon, 13 Jul 2026 08:53:27 +0000</pubDate>
      <link>https://dev.to/pankaj_batra/5-power-automate-design-mistakes-that-slow-down-your-flows-and-how-to-fix-them-292f</link>
      <guid>https://dev.to/pankaj_batra/5-power-automate-design-mistakes-that-slow-down-your-flows-and-how-to-fix-them-292f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on my Hashnode blog and is shared here for the Dev.to community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Power Automate makes it incredibly easy to automate business processes.&lt;/p&gt;

&lt;p&gt;With just a few actions, you can connect Microsoft Forms, SharePoint, Outlook, Teams, SQL Server, and hundreds of other services—often without writing a single line of code.&lt;/p&gt;

&lt;p&gt;But as workflows grow, something interesting happens.&lt;/p&gt;

&lt;p&gt;The automation still works...&lt;/p&gt;

&lt;p&gt;...yet it becomes slower, harder to debug, and increasingly difficult to maintain.&lt;/p&gt;

&lt;p&gt;In many cases, the issue isn't Power Automate itself.&lt;/p&gt;

&lt;p&gt;It's the workflow design.&lt;/p&gt;

&lt;p&gt;Over the past few months, while working on enterprise automation solutions, I've noticed the same design patterns appearing repeatedly. Individually they may seem harmless, but together they can significantly impact performance, readability, and long-term maintainability.&lt;/p&gt;

&lt;p&gt;In this article, I'll share five common Power Automate design mistakes and practical ways to avoid them.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Flow Design Matters
&lt;/h1&gt;

&lt;p&gt;Unlike traditional code, Power Automate workflows are visual.&lt;/p&gt;

&lt;p&gt;That makes them incredibly easy to build.&lt;/p&gt;

&lt;p&gt;Unfortunately, it also makes it easy to introduce unnecessary actions, nested loops, duplicated logic, and expensive operations without realizing their long-term impact.&lt;/p&gt;

&lt;p&gt;As workflows become more complex, these small design decisions compound.&lt;/p&gt;

&lt;p&gt;A well-designed workflow provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 Faster execution&lt;/li&gt;
&lt;li&gt;🐞 Easier debugging&lt;/li&gt;
&lt;li&gt;📈 Better scalability&lt;/li&gt;
&lt;li&gt;🧹 Simpler maintenance&lt;/li&gt;
&lt;li&gt;👥 Easier collaboration with other developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at five common mistakes.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Using "Apply to each" When You Don't Need It
&lt;/h1&gt;

&lt;p&gt;One of the most common performance issues is unnecessary &lt;strong&gt;Apply to each&lt;/strong&gt; loops.&lt;/p&gt;

&lt;p&gt;Power Automate automatically creates these loops whenever an action returns an array, even when you're expecting only a single item.&lt;/p&gt;

&lt;p&gt;Many developers simply leave the loop in place.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Items

↓

Apply to each

↓

Condition

↓

Update Item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this works, every additional loop adds unnecessary processing and makes the flow harder to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Approach
&lt;/h2&gt;

&lt;p&gt;If your logic expects a single record, retrieve the first result instead of iterating through the collection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Items

↓

First()

↓

Condition

↓

Update Item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Less processing&lt;/li&gt;
&lt;li&gt;Cleaner workflows&lt;/li&gt;
&lt;li&gt;Faster execution&lt;/li&gt;
&lt;li&gt;Easier debugging&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  2. Retrieving More Data Than Necessary
&lt;/h1&gt;

&lt;p&gt;Another common mistake is retrieving an entire SharePoint list and filtering the results afterward.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Items

↓

5000 Records

↓

Filter Array

↓

Find One Record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although functional, this forces Power Automate to process far more data than necessary.&lt;/p&gt;

&lt;p&gt;Instead, use &lt;strong&gt;OData Filter Queries&lt;/strong&gt; whenever possible.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status eq 'Pending'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title eq 'Vendor A'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows SharePoint to perform the filtering before returning the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Less data transferred&lt;/li&gt;
&lt;li&gt;Faster execution&lt;/li&gt;
&lt;li&gt;Better scalability&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. Performing Duplicate Checks After Creating Records
&lt;/h1&gt;

&lt;p&gt;Another design pattern I occasionally see looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Item

↓

Search for Duplicate

↓

Delete Duplicate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although functional, it performs unnecessary work.&lt;/p&gt;

&lt;p&gt;A cleaner approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search Existing Record

↓

Exists?

↓

Yes → Skip

↓

No → Create Item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prevents duplicate records&lt;/li&gt;
&lt;li&gt;Reduces unnecessary writes&lt;/li&gt;
&lt;li&gt;Improves performance&lt;/li&gt;
&lt;li&gt;Keeps data clean&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  4. Writing Extremely Complex Expressions
&lt;/h1&gt;

&lt;p&gt;Power Automate expressions are incredibly powerful.&lt;/p&gt;

&lt;p&gt;Functions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;split()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;concat()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;length()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coalesce()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;allow sophisticated transformations.&lt;/p&gt;

&lt;p&gt;The mistake is combining everything into one huge expression.&lt;/p&gt;

&lt;p&gt;Instead of writing something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concat(
 split(...),
 if(...),
 body(...)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Break the logic into multiple &lt;strong&gt;Compose&lt;/strong&gt; actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compose – Format Date

↓

Compose – Customer Name

↓

Compose – Final Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach makes workflows significantly easier to understand, debug, and maintain.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Never Looking at Flow History
&lt;/h1&gt;

&lt;p&gt;Many developers only check whether the flow succeeded.&lt;/p&gt;

&lt;p&gt;Flow History provides much more valuable information.&lt;/p&gt;

&lt;p&gt;It can help identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow-running actions&lt;/li&gt;
&lt;li&gt;Retry attempts&lt;/li&gt;
&lt;li&gt;Connector latency&lt;/li&gt;
&lt;li&gt;Failed expressions&lt;/li&gt;
&lt;li&gt;Workflow bottlenecks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewing Flow History regularly often reveals optimization opportunities that aren't obvious while building the workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Best Practices I Follow
&lt;/h1&gt;

&lt;p&gt;Whenever I build a Power Automate workflow, I try to follow a few simple principles.&lt;/p&gt;

&lt;p&gt;✅ Keep workflows as linear as possible.&lt;/p&gt;

&lt;p&gt;✅ Filter data before retrieving it.&lt;/p&gt;

&lt;p&gt;✅ Avoid unnecessary loops.&lt;/p&gt;

&lt;p&gt;✅ Break complex logic into smaller Compose actions.&lt;/p&gt;

&lt;p&gt;✅ Prevent duplicate records before writing data.&lt;/p&gt;

&lt;p&gt;✅ Review Flow History regularly.&lt;/p&gt;

&lt;p&gt;Small improvements made early can dramatically improve maintainability as workflows evolve.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Power Automate makes automation accessible to everyone.&lt;/p&gt;

&lt;p&gt;Designing workflows that remain clean, scalable, and performant as they grow is a different skill.&lt;/p&gt;

&lt;p&gt;Most performance improvements don't come from changing connectors.&lt;/p&gt;

&lt;p&gt;They come from making better design decisions.&lt;/p&gt;

&lt;p&gt;A well-structured workflow is easier to understand, easier to debug, and much more reliable as business requirements evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Pankaj Batra&lt;/strong&gt;, a Software Engineer focused on building scalable software, automation solutions, and enterprise integrations.&lt;/p&gt;

&lt;p&gt;I regularly write about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ Power Automate &amp;amp; Workflow Automation&lt;/li&gt;
&lt;li&gt;📱 Flutter &amp;amp; Mobile Development&lt;/li&gt;
&lt;li&gt;🐍 Python &amp;amp; Backend Development&lt;/li&gt;
&lt;li&gt;🔗 REST APIs &amp;amp; System Integrations&lt;/li&gt;
&lt;li&gt;🏗️ Software Architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Connect with me
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Portfolio: &lt;a href="https://pankajbatra.vercel.app" rel="noopener noreferrer"&gt;https://pankajbatra.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 LinkedIn: &lt;a href="https://linkedin.com/in/pankaj-batra-0a294a205" rel="noopener noreferrer"&gt;https://linkedin.com/in/pankaj-batra-0a294a205&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://github.com/Pankaj0405" rel="noopener noreferrer"&gt;https://github.com/Pankaj0405&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this article helpful, consider following me for more practical software engineering content.&lt;/p&gt;

</description>
      <category>powerautomate</category>
      <category>sharepoint</category>
      <category>automation</category>
      <category>microsoft</category>
    </item>
  </channel>
</rss>
