Skip to content

Personal project · live, in use · Sep 2026

Study

Turn your notes into flashcards that cite the page they came from

An installable study app that turns a student's own notes into flashcards and quizzes, shows the page every answer came from, and grades written answers against a rubric — built for five real users on free tiers, with every product decision written down before it was built.

TypeScriptReact Native WebExpo RouterSupabasePostgreSQLGeminiZodVitestCloudflare Pages
tests passing
447
lines of TypeScript
13.3k
logged product decisions
14
tables, all RLS-guarded
11
monthly running cost
$0
lines of design docs
3.3k

01

The product in one sentence

"I give the app my notes. It asks me about them, shows me where every answer came from, remembers what I got wrong, and grades my written answers."

The first two are table stakes — NotebookLM already does them. The last two are the product, and they are the reason this exists.

I wrote the spec before writing the app: fourteen numbered decisions, each with what changed from the original brief and why. Several of them are refusals. Fill-in-the-blank was cut because exact-match grading of free typing frustrates you when you type "ATP" and the answer key says "adenosine triphosphate". A second AI verification pass was cut in favour of deterministic validators, because those are free, instant and testable.

02

Every answer carries its source

Each card shows a chip — Source · p.14 — that opens a short excerpt from the student's own notes, with the matched phrase highlighted.

That highlight is why the excerpt validator returns a span rather than a boolean. It has to check that the answer really appears in the source anyway, so it returns where, and the UI reuses that span instead of searching the text a second time and possibly disagreeing with the validator that approved the card.

The chip is always visible; the excerpt is collapsed behind it. The chip is the product's promise, so it has to be legible without hunting — but the excerpt answers a question you have not asked yet, since most of the time you read the verdict, agree, and move on. The one thing that stays outside the collapse is the flag warning that your notes may contradict standard knowledge. A warning nobody opened is not a warning.

Understand · 12 of 25

Why does the AV node delay the impulse before it reaches the ventricles?

So the atria can finish contracting and empty into the ventricles before the ventricles contract.

Source · p.14

…conduction slows markedly through the AV node, and this delay allows atrial contraction to complete before ventricular systole begins, optimising ventricular filling…

MissedGot it
A revealed card — the source chip is always visible, the excerpt opens behind it

03

The model never decides a score

Short written answers are graded against a rubric of expected concepts, but the model's only job is to say which of those concepts it found. The arithmetic, the thresholds and the verdict are all computed in code.

So the same answer always scores the same, the scoring is unit-testable without a model, and an empty answer cannot talk its way to full marks — which is exactly what a model asked to "grade this out of 5" will occasionally do.

Generated cards go through deterministic validators before a student ever sees them: schema, excerpt-actually-in-source, multiple-choice sanity, answer-leak, and dedup. No second LLM pass. With five users, "Report this card" is the real second opinion.

Short answer

2 / 3

“It slows the signal down so the atria empty first.”

Delay allows atrial emptying

Protects ventricles from fast atrial rates

AV node is the only normal electrical path

Close. You also want the AV node as the only normal route between atria and ventricles.

A graded written answer — the model reports concepts found; the score is computed in code

04

Free tiers have sharp edges

The whole thing runs on free tiers, and most of the architecture is shaped by what those tiers actually do rather than what they advertise.

The study assistant is capped at twenty questions per person per day, and the count is claimed in the database before the model is called — never in the browser. A browser counter would protect nothing, because the quota is per Google project: a phone and a laptop would each get a full allowance and between them exhaust the same pool that card generation depends on. Generation is the product, so an afternoon of chatting must not cost someone their cards. For the same reason the assistant keeps no history — every follow-up would re-send the whole thread, so the third question costs several times the first while drifting further from the notes it is meant to answer from.

Supabase Free includes no automatic backups, so a scheduled job takes a monthly dump. Free projects also pause when idle, so a second job keeps the database awake. And sign-in is a six-digit code rather than a magic link, because a magic link opens in Safari rather than the installed app, and the session never lands where the user is.

05

Saying the uncomfortable thing plainly

Notes are sent to Google using the student's own free key. The free tier's terms say human reviewers may read and annotate that input, and that Google may retain it to improve its products.

So the app says exactly that, in the words a person would use: "Someone at Google may read them, so please don't add anything private." It sits above the input and next to the key field — not behind a link, not in a collapsed panel.

The spec marks one sentence as the part that must survive any later edit for brevity: a real person at Google may read them. It is the fact users are least likely to assume on their own, and it is the first thing that would be cut for tidiness.

06

Things typecheck could not see

The test suite runs 447 tests, and two of them exist because a green suite is not the same as a working app. One asserts there is exactly one copy of React in the bundle, because two produce a blank white page. The other executes the built bundle in a DOM and asserts that real content appears — the deploy-shaped failure that every unit test passes through.

There is also a screenshot script, written after typecheck and the test suite missed three UI defects in a row: a diagram cropped to its top band, an image that collapsed to nothing, and a header that showed the user the literal text "set/[id]/blanks". All three were obvious the moment someone looked. It serves the real build, injects a session, and drives Chrome over the DevTools Protocol with no dependencies at all.

The chart colours on the progress screen went through a contrast validator. My first attempt reused the UI border colour as a chart fill and failed at 1.27:1 against the card — that segment was effectively invisible. The palette that shipped clears 3:1 on every fill and separates by ΔE 16.1 under deuteranopia, with every segment labelled in text so identity never rests on colour alone.