Skip to content

AI Product Engineering · Generative AI

TubeMate

An AI YouTube learning assistant, on the web and as a Chrome extension. It pulls a video’s transcript, generates a structured summary, and answers follow-up questions about the content.

Status
Live
My role
Product & engineering
Stack
FastAPI · Postgres · Redis
Model
Gemini 2.5 Flash Lite

The problem

People use YouTube to learn, but long-form content is a poor reference format. Finding the one section of a two-hour talk that matters means scrubbing through it, notes taken while watching are fragmented, and there is no way to interrogate a video after the fact.

That was my starting hypothesis, not a validated market finding. I built the first version to test it. What the usage data eventually told me was more complicated than the hypothesis, and that is the more interesting part of this case study.

What I owned

I own product and technical direction, and I wrote the backend, the web frontend and the Chrome extension. That covers the transcript pipeline, the summarisation chain, the chat layer, authentication, rate limiting and abuse controls, Stripe billing, the analytics and error-tracking setup, and deployment.

Two collaborators work on distribution, content and user acquisition. The engineering is mine; the growth work is not.

Architecture

A request arrives from the web app or the extension, hits FastAPI, and the backend fetches the transcript and detects its language. Redis is checked for a cached result. On a miss, the transcript goes to Gemini in a single prompt and the result is stored in Postgres against the user’s history.

TubeMate request architectureA request from the web app or Chrome extension reaches a FastAPI backend. The backend fetches the YouTube transcript, detects its language, and checks a Redis cache. On a miss it selects one of five prompt templates by keyword heuristic and sends the whole transcript to Gemini 2.5 Flash Lite in a single prompt. Summaries and history are stored in PostgreSQL. Follow-up chat passes the stored summary or transcript back into the model as context. There is no embedding or vector retrieval step anywhere in the system.Web appstatic ES modules · nginxChrome extensionManifest V3FastAPIauth · rate limits · routingTranscript fetch+ language detectionRediscache · rate limitingPrompt template1 of 5, keyword heuristicGemini 2.5 Flash Litefull transcript, single promptPostgreSQLusers · summaries · historySupporting servicesStripe · Resend · Sentry · PostHog
The request path as built. Note what is absent: no embedding step, no vector index, no retrieval loop.

Decision: long context instead of retrieval

The obvious architecture for “ask questions about a document” is retrieval-augmented generation: chunk the transcript, embed the chunks, store them in a vector index, and retrieve the relevant ones per question. I decided against it, and it is the decision I get asked about most.

The reasoning was that the premise for RAG did not hold here. A typical target video runs 15–90 minutes, which is roughly 3,000–20,000 tokens of transcript. That fits inside Gemini 2.5 Flash Lite’s context window with room to spare. Retrieval solves the problem of content that cannot fit in context. I did not have that problem.

Adopting it anyway would have meant an embedding call on every video, a vector index to provision and keep consistent with Postgres, a retrieval step adding latency to every question, and a new failure mode where the right chunk simply is not retrieved and the model answers confidently from the wrong context. In exchange for that, I would have gained nothing a user could perceive, since the model can already see the whole transcript.

So summarisation stuffs the full transcript into one prompt, and chat passes the stored summary or transcript back as context. The free and premium tiers cap videos at 30 and 120 minutes respectively — a product decision that is really a technical one, since the cap is what keeps transcripts inside the context window.

Decision: routing prompts by content type

The first version used one summarisation prompt for everything. It worked acceptably for lectures and tutorials and produced poor structure for podcasts and interviews, where there is no thesis to extract and the value is in the exchange.

I added a detection pass that runs a keyword heuristic over the transcript and selects one of five prompt templates: default, educational, tutorial, entertainment, or conversation. A video opening with “welcome back to the show” is summarised differently from one opening with “step one”.

I chose keywords over asking the model to classify the content first. Classification would have meant a second model call on every summary, roughly doubling cost and adding latency, to solve a problem that a list of phrases solves most of the time. The heuristic is crude and misroutes edge cases, and that is a trade I am comfortable defending.

Output structure is enforced by prompt instruction alone — the prompt asks for specific markdown headings and the response passes through a plain string parser. There is no schema and no function calling. If I were hardening this, structured output would be the first change, because prompt-instructed formatting fails occasionally and nothing downstream catches it.

Decision: layered registration abuse controls

After launch, registrations arrived faster than any distribution work justified. The accounts were real enough to pass validation and never verified their email addresses.

I treated it as a funnel problem rather than a security one. Rather than block registration harder, I made unverified accounts worthless: a require_verified_email dependency gates product use, so an unverified account can exist but cannot generate anything. That removed the incentive without adding friction for real users.

Layered around that, what runs today:

  • Per-IP registration limit: 5 attempts per hour
  • Per-IP login lockout: 10 failures per hour
  • Browser fingerprint plus IP limit on the public summary endpoint: 3 per day
  • Verification email resend limit: 1 per 60 seconds

The fingerprint limit exists because the public endpoint is the one path that works without an account, which made it the obvious target once the verification gate closed the other one.

What the numbers showed

Signups
224Total accounts created
Verified
11853% of signups
Activated
63Ran at least one summary — 53% of verified
Videos processed
94Unique videos
Summaries
130Across those 94 videos
Content processed
~78.6 hrsFrom stored durations
Chat messages
54
Paid subscribers
1Active Stripe subscription
Returning users
Not measuredlast_login_at is never written — see below
Measured 19 August 2026. Figures come from the production database; anything I cannot source reliably is marked as not measured rather than estimated.

The signup number was telling me nothing

Weekly signups after launch ran between 1 and 12. In mid-May they climbed sharply — 30, then 46, then 28 across three weeks. Then the following week they collapsed to 2, and have run between 2 and 9 since.

The 46-signup week looked like the product working. Set against the full funnel, it was not: 224 people have registered, 118 verified an email address, and 63 have ever generated a summary. Nearly half of everyone who signed up never confirmed they were a real person, and only a quarter did the one thing the product exists to do.

The number I had been watching was the one least connected to whether anyone found the product useful. Signups measure curiosity and are trivially inflatable by bots and disposable addresses. The verified-to-activated conversion is the honest measure, and at 53% it is a genuinely reasonable rate — the problem was never activation, it was that I was reporting the wrong denominator to myself.

I now treat activated users as the headline number. It is smaller and considerably more useful.

Three production problems

YouTube rate-limiting cascade. Between June and August, roughly 70 Sentry events traced to the same cause: once transcript volume passed a threshold, YouTube served the server’s IP a 429 behind a CAPTCHA gate. The transcript library retried immediately against the same 429, which sustained the block and generated the error cascade. I added a Redis-backed five-minute cooldown so a 429 stops the retry loop instead of feeding it. Fixed.

Retention data was never being recorded. While pulling the numbers above, I found last_login_at is null for every user in the database. Neither the password path nor Google OAuth writes it on successful authentication. The consequence is that I have no returning-user data for the entire life of the product, and no way to reconstruct it. The metric grid above says “not measured” for that row, and that is why.

A paying customer was not getting what they paid for. Cross-referencing Stripe against the database showed the one active subscriber carrying plan_type = 'free'. The webhook receives the subscription event but the handler does not sync the plan field, so the account was billed monthly while limited to free-tier caps.

How I check quality, and how I should

Honestly: manually. I spot-check summaries during development and after changing a prompt template. The automated tests cover the pipeline up to the cache layer, and the model-invoking chains themselves are exercised by hand against the live model. There is no golden dataset, no regression suite for output quality, and no automated hallucination check.

That was survivable while I was the only person changing prompts, and it is the weakest part of the system. The content-type routing exists because I noticed bad podcast summaries by eye — which means the discovery path for quality regressions is currently “Mayowa happens to look”.

What it needs is a fixed set of test videos with expected section coverage, scored by rubric on each prompt change, plus a grounding check on chat answers against the transcript. That is the next engineering investment I would make, ahead of any new feature.

What I took from it

Pick the architecture the problem needs, not the one the category expects. RAG was the default answer for this product shape and it would have been the wrong one. Being able to explain why I did not build something has been more useful in conversation than the features I did build.

An unwatched metric is worse than no metric. I tracked signups because they were easy to see, and the number moved in a way that felt like progress while telling me nothing. Two of the three bugs above were found by finally interrogating my own instrumentation.

After launch, reliability beats features. The work that mattered most in the last few months was a cooldown, a verification gate and a rate limit — none of which are visible to a user who is having a good time, and all of which are the reason they are.

What I would do differently

Build the evaluation harness before the second prompt template. The moment there was more than one prompt path, I needed a way to tell whether a change to one regressed another. I still do not have it.

Instrument the funnel, not the front door. Verified and activated counts should have been the dashboard from day one. Writing last_login_at is a two-line change I never made because nothing forced me to look.

Test the billing path like it has someone’s money in it. The Stripe webhook was verified as “receives events” and never as “produces the correct account state”. An integration test against a test-mode subscription would have caught it before a customer did.

Use structured output. Prompt-instructed markdown headings work until they do not, and nothing downstream validates the shape.

Evidence

The product is live and the extension is publicly listed. Figures above come from the production database on 19 August 2026; incident counts come from Sentry.

TubeMate landing page. A summary panel shows Key Insights as three bullet points, followed by a question and a grounded answer drawn from the video.
The product surface: paste a link, get structured Key Insights, then ask follow-up questions answered from the video's content.
A grid of six user testimonials, each naming the person, the video they used and the outcome they got.
User feedback collected in-product. One tester's note — that it 'doesn't hallucinate' — is the kind of claim I would rather verify with an evaluation harness than rely on.
Screenshot to be added
Chrome extension running on a YouTube watch page.