case_study.mdCase study
KeepForLater
Turning information overload into queryable knowledge
An “AI second brain” SaaS designed, built and shipped solo, from architecture to billing. This page details the engineering decisions that keep an AI system running in production — not just in a demo.
See the live product- Role
- Personal product, solo at the helm — architecture, code, production
- Surfaces
- SSR web + PWA · browser extension · MCP server
- Technical core
- Durable LLM workflows · hybrid RAG · structured outputs
- Operations
- Per-call telemetry · continuous evals · separate environments
The problem
We pile up hours of videos, podcasts, articles and newsletters we will never revisit. The information is saved, but it never becomes knowledge: you cannot retrieve the exact argument you heard three months ago, let alone quote it with its source.
Generic assistants miss the point here: they do not know your sources, they invent references and cannot say where a claim comes from. So the challenge was never to plug in a language model, but to build a full chain — ingestion, distillation, indexing, sourced answers — that stays reliable and whose cost stays predictable.
The constraints
- An AI job that fails halfway must never re-bill the steps that already succeeded.
- Every answer must be traceable to the exact passage in the source. Without a verifiable citation, an answer is worthless.
- Cost per action must be known before it is billed, otherwise the business model reveals itself at month end.
- One user’s data must never cross another’s, including inside the vector index.
- One developer: every choice had to shrink the surface to operate, never grow it.
The architecture, step by step
Ingestion: a workflow that survives failure
Ingesting a one-hour video chains five slow, expensive operations: extraction, distillation, categorization, vector computation, finalization. Written naively, they form one long function where a single network error throws away work that was already paid for.
Each step is therefore a durable workflow step, checkpointed and idempotent. A failing step is retried on its own, with the previous results already persisted: if vector computation fails, transcription is not re-run — and therefore not re-billed.
- Long media transcribed via byte-range segmentation, working around the model output limit without degrading quality.
- Seven automated watch types (YouTube channels and playlists, subreddits, RSS feeds, keywords, weekly digest) triggered by scheduled tasks.
Distillation: the model returns JSON, not prose
The whole distillation — short summary, key points, concepts, narrative summary, timestamped chapters — is emitted as schema-validated JSON. The schema is declared once and shared across server, client and extension: if a field changes, the compiler flags it everywhere before deployment.
The model also self-assesses its faithfulness to the source, and that score is surfaced in the UI. Users can tell when a summary drifts from the original material, instead of having to take it on trust.
Retrieval: hybrid, because dense search alone fails
Dense search alone misses exactly what matters most: a proper noun, an acronym, an exact reference. Lexical search alone misses meaning. So both run in parallel and their results are fused by reciprocal rank, surfacing the passages both methods consider relevant.
Text is split into overlapping semantic chunks so that an idea straddling two paragraphs is never cut in half. Results are then diversified per source, so a single verbose document cannot monopolize the answer.
Every answer cites its sources, and clicking a citation opens the exact passage, highlighted. Search crosses languages: a question asked in French finds an English passage, by meaning.
- A replayable evaluation harness measures retrieval quality on every change — a regression shows up before release, not after.
- Per-user isolation is enforced at the SQL query level, including on the vector index.
Exposure: three surfaces, one contract
The product lives on three surfaces: a server-rendered, installable web app, a browser extension to capture a page in one click, and an MCP server. All three call the same end-to-end typed interface — the client knows the server routes at compile time, with no generated code and no documentation to maintain.
The MCP server is the most structural choice: it exposes the knowledge base as a set of tools usable straight from Claude, ChatGPT or a code editor. Users do not have to come to the app — they bring their own assistant.
- Active retention: on-demand quizzes, spaced repetition, and a knowledge graph linking sources by co-occurrence and semantic proximity.
- Text-to-speech with edge caching and a private podcast feed, to replay your own summaries on the move.
Economics and compliance: measure before billing
An AI product breaks on its costs before it breaks on its engineering. Every model call reports its real consumption, recorded per action. The credit system is calibrated on that measured cost: margin is known before the invoice, not discovered after.
On the compliance side, analytics and error-tracking tools only load after explicit consent, and account data is partitioned and genuinely deleted on request.
Operations: making the pipeline measurable
A production AI system raises three questions code alone cannot answer: what an action truly costs, which prompt produced a given result, and whether quality has dropped since the last release. Three questions, three mechanisms.
Telemetry writes one row per call actually emitted. Failed calls included: the provider bills tokens for a generation that dies mid-flight, and hiding them would understate cost. An unknown cost is recorded as unknown, never as zero — a zero would read as “free call” and skew every downstream total. Writes are batched and off the critical path: an observability outage loses the measurement, never the ingestion the user paid for.
Prompts stay in the repository — Git is the versioning. A registry stores only their identity: id, version, text fingerprint. On every integration run, the check recomputes the fingerprint of the prompt actually sent to the model and rejects an edited text that kept its version number. This is the linchpin: without that guard, before-and-after measurements blend under one version and the history becomes worthless.
Evaluation runs on three layers. Deterministic, free checks on every contribution: length, structure, language, and above all grounding — does every concept and every figure in the summary exist in the source? That is an objective, checkable hallucination signal. Then an independent judge, on a different model from the one under test. Finally a campaign that asks not “is this good?” but “at what point does it break?”.
- Traces are exported to an external observability tool, asynchronously and off the critical path. The official client will not boot on this runtime: since the ingestion API is a plain authenticated call, it is invoked directly — about a hundred lines, no dependency.
- An exporter that swallows its errors by design cannot be assumed to work, it must be proven: an end-to-end test sends a trace through the exact production path, reads it back with its observations, and names the project the keys belong to — the classic cause of “the test passes but the dashboard is empty”.
- Development, staging and production each tag their own traces: a regression caught in staging no longer pollutes production dashboards.
- Deleting an account anonymizes measurements rather than erasing them. Erasing would retroactively change the cost of an already-closed month on every departure — accounts that rewrite themselves make margin impossible to track.
What measurement revealed
Six analyses, and what each one changed in the product
A setting the model did not honor
- Observed
- On the deep level in quality tier, model reasoning burned 9,300 to 11,400 tokens against a budget set at 6,144. It accounted for 77% of billed output tokens and filled 80% of the ceiling.
- Diagnosis
- The numeric control belongs to the previous model generation; the current one treats it as a preference, not a bound. Concretely: a slightly denser source truncated the output, and the user lost their summary after being charged.
- Decision
- Speak the vocabulary the model actually honors, instead of raising the ceiling. Raising it would have moved the problem onto margin: at the higher ceiling considered, a summary cost more than it earned.
- Outcome
- 31% lower cost on the quality tier, with fidelity up.
A single measurement was lying
- Observed
- The same configuration, measured twice in a row, produced 9,539 then 3,005 reasoning tokens — 75% then 34% of the same budget.
- Diagnosis
- With that variance, one measurement says “act now” on Monday and “all clear” on Tuesday. The risk was there from the first run; it was merely invisible.
- Decision
- Every cell of the evaluation matrix is repeated, and the worst run is the one that counts. An alert threshold is set on the worst case, never on the average.
The model refused to write as long as promised
- Observed
- On a 150,000-character source, the deepest level promised 2,500 to 4,500 words and delivered a quarter of that. The ceiling was not to blame: generation used only 2–3% of the available room.
- Diagnosis
- Past a certain length the model simply stops, however insistent the instruction — including when it frames the target as a failure condition.
- Decision
- Split the source into windows and ask for one section per window: a short target the model honors. Each window is read once, so input cost stays that of a single full read — only output grows, which is exactly what the premium tier should pay for.
- Outcome
- Output length tripled, contract finally met — and a 95th-percentile cost that went down, because short outputs are more predictable than one generation pushed to its limit.
Two reinforced prompts, two failures
- Observed
- The deep level was supposed to return three to six titled sections; the model returned one block. First attempt: the instruction frames structure as a failure condition. Failed. Second: the instruction moves onto the field description itself. Failed.
- Diagnosis
- An instruction does not constrain, it suggests. No rewording turns a suggestion into a guarantee.
- Decision
- Make the structure impossible to ignore rather than strongly recommended: the generation schema requires an array of sections, and the code assembles the final render. The model can no longer return a single block.
- Outcome
- Structure compliant across every evaluated case, without a single extra call — and a test locks the invariant whatever the content.
A billed model that did not exist
- Observed
- The quality tier failed in production with a “resource not found” error — after the user had been charged.
- Diagnosis
- The model identifier, taken from the documentation, was not the one the API actually accepts. A hard-coded identifier can also be renamed or withdrawn without notice.
- Decision
- A command checks configured identifiers against what the key really grants. The next disappearance will surface before deployment, not on an already-paid user action.
The score a model gives itself is worthless
- Observed
- Distillation produced a fidelity score… awarded by the very model that had just written the text.
- Diagnosis
- Structurally self-serving, and manipulable by injection from ingested content: a hostile source can literally ask for a high score. That is not an evaluation, it is an opinion.
- Decision
- A judge on a different model, whose gap with the self-assessment becomes the interesting signal. But a judge has biases too — it favors long, well-written text — so its scores are checked against human ratings. Past a 0.75-point gap out of 5, it mostly measures its own preferences: an uncalibrated judge is an unstandardized thermometer.
Engineering decisions
What was chosen, why, and what was ruled out
Durable workflow over a hand-rolled job queue
Resuming at the failed step is native, with no state table to maintain and no replay logic to debug.
ruled out: Custom queue + status table
Vectors inside Postgres over a dedicated vector database
One database to operate, back up and secure — and per-user isolation happens in the same query as the business joins.
ruled out: External vector service
Hybrid retrieval over pure dense search
Dense search alone misses proper nouns and acronyms — precisely what users type when they are looking for something specific.
ruled out: Vector similarity only
Schemas shared across server, client and extension
A single definition is authoritative: types break at compile time instead of breaking in production on a renamed field.
ruled out: Types duplicated on each side
MCP server over a proprietary API
Users plug in the assistant they already use, with no bespoke integration to write for each one.
ruled out: In-house API + one-off integrations
Credits calibrated on measured consumption
Pricing follows the real cost of each action instead of an estimate, making margin predictable from the first invoice.
ruled out: Flat plan based on guesswork
Observability alongside, not in-line
A gateway sits between the app and the model: its outage becomes ours, its latency adds to every billed call, and source content transits through a third party. Here measurements land in the database first; the external tool is only a view.
ruled out: In-line proxy gateway
Prompts in the repository, not in a dedicated tool
Fetching them at call time would add a network round trip and a failure mode to a billed path. The only upside — editing without deploying — is worthless with no non-technical team to serve. Git versions, a guard verifies, the external tool receives a mirror.
ruled out: Remote prompt manager
What this project demonstrates
Wiring up a language model takes an afternoon. What takes craft is everything around it: failure recovery, traceable answers, schemas that hold, measured costs, partitioned data. That is exactly what I design for clients — the same rigor as on banking environments, applied to AI systems.
Every fix above came from a measurement, not a hunch — and two of them began with a failure the repository history still holds. A system you do not measure does not decay more slowly: it decays without anyone noticing.
Stack
Got a project that looks like this one — search over your own data, AI workflows, a product to ship end to end?