5 September 2026 · 9 min read
“Done” Was Never the Right Word


I'm the co-founder of Quero, a NEET/JEE exam-prep platform. What sounded like a small ask, “the math in our questions looks typed, can you make it look like a real exam paper?”, turned into a multi-day chain of fixes, and every single one of them revealed a smaller problem sitting underneath the one I'd just closed. That's the part worth writing down, not the LaTeX itself.
The request that looked finished in one sentence
The screenshots he sent were real JEE/NEET paper scans: fractions, integrals, superscripts, the works. The app's question bank showed the same content as flat strings, x^2 and 1/2 instead of typeset math. The fix sounds like a UI problem: add a rendering library, done by lunch.
It wasn't, because the deeper issue wasn't how the text was displayed. It was what the text actually contained.
Layer one: there was nothing to render
I checked the database before touching any component. Zero LaTeX packages installed, and the stored question text was plain ASCII math, eps0, x^-2, root3, not a single dollar-sign delimiter anywhere. You cannot render LaTeX that was never written. The real first task wasn't a rendering component, it was rewriting the underlying content to contain real LaTeX in the first place.
That's the first shape this problem kept taking: the visible symptom (“math looks ugly”) and the actual defect (“math isn't math, it's text pretending to be math”) were two different layers, and fixing the top layer without the bottom one would have shipped nothing.
Layer two: the AI rewrite that quietly ate its own output
The plan was to have an AI pass rewrite each question's text into real LaTeX, wrapped in $...$ delimiters, then store it back. First batch: JSON parse errors on some rows, and worse, no errors on others, just corrupted text. \text{VSD} was silently becoming a tab character followed by the letters ext{VSD}.
The reason is a genuinely nasty JSON fact: \t, \f, \n, \r, \b, and \u are all syntactically legal JSON escape sequences. A JSON parser has no way to know that \text was meant to be a LaTeX command and not an actual tab character followed by literal text. It will happily parse it wrong and hand you back garbage with zero error thrown. My first two attempts at a fix, a smarter regex that tried to guess which backslashes were “real” escapes, both failed on exactly this. The eventual fix: escape every backslash unconditionally before parsing, accepting that a genuinely intentional newline inside a question would come through as literal text instead. A narrower, deliberate trade-off beat a clever heuristic that silently lied.
The scariest bugs in this whole project weren't the ones that threw errors. They were the ones that returned a plausible-looking result that was quietly wrong.
Layer three: fixing the wrong database
Partway through, I ran the whole migration end to end, verified it, called it done. Then it turned out I'd been pointed at a stale duplicate Supabase project, not the one the live site actually reads from. Every question I'd “fixed” was invisible to real users. The actual .env.local pointed somewhere else entirely.
Nothing about that mistake was visible from inside the work itself. The migration succeeded, the data looked right, the queries returned exactly what I expected. The defect was one level outside the code: which target the code was even pointed at. That's a different category of “not actually done” than a parsing bug, and it's the one that's easiest to miss because success and failure look identical from where you're standing.
Layer four: a teammate adds questions, and a new gap appears
Once the pipeline worked, I brought in a teammate to bulk-add practice questions through their own Supabase-connected AI session. New questions went in clean, correct LaTeX, right subject and chapter. And then: invisible on the site. Again.
The cause this time was a single mismatched string. The app's filter dropdown expects the exact value NEET-UG. The new rows had been tagged NEET, or CBSE AIPMT, the exam's real historical name from before it was renamed in 2013. Reasonable labels, wrong string, and Postgres doesn't throw a warning for “value technically valid, filter still won't match it.” It just returns zero rows, silently, forever.
- A missing feature (no LaTeX rendering) can hide a missing precondition (no LaTeX in the data at all).
- A parser that doesn't error isn't a parser that succeeded. JSON's legal-but-wrong escape sequences turned a formatting pass into silent data corruption, twice, before the fix held.
- “It works” and “it works against the right target” are two separate claims. Verify the second one explicitly, don't infer it from the first.
- Enum-shaped fields (an exam name, a status, a category) need an explicit contract, not a reasonable guess. “NEET” and “NEET-UG” are the same exam to a person and two different, silently non-matching values to a WHERE clause.
- Every fix in this project surfaced a smaller, more specific version of the same root problem: something looked complete because nothing was throwing an error, not because it was actually correct.
Why none of this showed up as an error message
That's the actual thread connecting all four layers. A missing LaTeX pipeline doesn't crash, it just renders flat text. A JSON escaping bug doesn't always throw, it sometimes just corrupts silently. A wrong database target doesn't warn you, every query still returns a result. A mismatched exam string doesn't error, the filter just returns nothing. Every single failure mode in this project was quiet by default. The system never told me something was wrong. I had to go looking, specifically for the case where success looked identical to failure.
That's a bigger lesson than any one bug: the failure modes worth actually worrying about are the ones that don't announce themselves. An exception is a gift, it tells you exactly where to look. A quiet wrong answer costs you the time to notice it's wrong at all, and by then it's already been shipped, viewed, and trusted.
The stack behind the fix
For anyone curious what actually shipped underneath all of this:
- Next.js 16 with React 19, the app's existing frontend, untouched in architecture, extended with a shared MathText component that parses $...$ / $$...$$ / \(...\) / \[...\] delimiters.
- KaTeX for the actual math typesetting, chosen for being fast enough to render inline in a live exam UI without a layout stutter.
- Supabase (Postgres) as the database, accessed both through the app's normal client and, for this migration work, directly via SQL, including altering Postgres functions (RPCs) that fed pre-shaped JSON to the frontend so image and LaTeX fields didn't need a second round trip.
- Google Gemini (flash-lite) for the bulk LaTeX-rewrite and dedupe pass over existing questions, run with a fallback to a best-effort plain-text prettifier for any row the model couldn't confidently rewrite.
- Inline SVG, generated directly and stored as base64 data URIs in the database, for the handful of questions that needed an actual diagram (a distance-time graph, a v-t curve) rather than a wall of symbols.
The takeaway
“Make the math look right” turned into: write real LaTeX where there wasn't any, fix a JSON-escaping bug that corrupts silently, confirm the fix was even pointed at the right database, and lock down an exam-name contract so the next person's clean, correct work doesn't vanish the same way mine almost did. None of those four things were visible from the outside. All four were exactly as necessary as the one everybody could see.
“Done” was never really the right word for any layer of this. Just done for now, until the next quiet gap showed up.