Personal product, 2026
Everything: mobile app, API, admin panel, database, scoring engine and content pipeline
Kasoti
A Gujarati-first exam prep app where a two-hour paper survives a dropped connection, a crash and a flat battery.
In development
A mock-test app for a state competitive examination, built from scratch for my sister while she prepares for it. The questions are not the product. Surviving the paper is, and so is the analysis afterwards, which is why scoring lives in its own package with no imports and its own unit tests.
The engineering goal that shaped everything else: a student halfway through a two-hour paper must not lose it. Not to a dropped connection, not to a flat battery, not to the operating system killing the app. Every answer, every flag and every navigation is written to on-device SQLite at the moment of the tap rather than on the way out of a screen, so the worst case is losing the tap in progress.
The admin side closes the content loop. A Python extractor pulls questions out of the examination commission’s own previous-year PDFs, a CSV importer validates and rejects rather than warning, and a command line tool assembles and publishes a finished paper. Development was AI-assisted; the architecture, the data model and the engineering decisions below are mine.
Surfaces
- mobileReact Native and Expo
Where she sits the papers. The paper being sat and the attempt in flight live in on-device SQLite, so the player never waits on the network between questions.
- React Native
- Expo
- Expo SQLite
- Drizzle ORM
- React Navigation
- TanStack Query
- Zustand
- i18next
- serverHono
Serves the content manifest and receives finished attempts. Better Auth mounted from a shared package.
- Hono
- Better Auth
- Drizzle ORM
- Postgres
- Zod
- adminTanStack Start on Nitro
Question bank, papers and accounts. Where an uploaded previous-year PDF becomes a publishable paper.
- TanStack Start
- Nitro
- Tailwind
- Postgres
- Drizzle ORM
What it does
- Previous-year question papers and full-length mock tests
- A timed test player with per-question flagging, review marking and a question grid
- Progress that survives a lost connection, a crash or a closed app mid-paper
- Post-test analysis: scoring, subject and topic breakdown, and a running error log
- Gujarati and English interface with custom fonts for both scripts
- Light and dark themes, and haptic feedback on every answer interaction
- Admin upload of a previous-year PDF, turned into structured questions and answers
- Option E, "Not attempted", on every question, because a question left blank is penalised exactly like a wrong one
- A pre-test briefing that states the marking scheme, the blank penalty and the marks by section before the clock starts
- Content language switchable mid-paper on a bilingual paper, without touching the interface language
- Analysis that attributes every lost mark: confident-wrong, guessed wrong, wrong-untagged, and forgone by skipping
- A speed-against-accuracy plot per section, with her own medians as the crosshairs
- Question-by-question review with explanations, filtered by wrong, skipped, marked, slow or lucky-correct
Engineering notes
- Online-only by decision and device-resident by construction: an account is required and nothing is reachable signed out, but the paper being sat and the attempt in flight are held in SQLite, so a dropped connection cannot cost a paper that is already open
- Answers are persisted on tap, not on screen exit — a SQLite write is about a millisecond, and a lost two-hour attempt is her evening
- A CI check enforces that the on-device schema stays a subset of Postgres, so the phone can never drift ahead of the server
- Content sync is version and checksum driven, so a paper whose content never changed is never downloaded twice
- scoring is a pure package with no imports and its own unit tests, which is what makes the analysis trustworthy
- One Zod validation layer shared by the CSV importer and the API boundary, so bad content is rejected at both ends by the same rules
- Interface strings route through i18next; question content deliberately does not, because a translated question is a different question
- Pooled and direct Postgres connection strings kept apart, because PgBouncer cannot serve transactions
- Option E is a response status, not a fifth option: every question in the bank has exactly four, and E resolves to a null selection with a not-attempted status — modelling it as an option would corrupt every accuracy figure
- Marks are doublePrecision in Postgres and real in SQLite, never numeric: Drizzle returns numeric as a string on one and a number on the other, and the same scoring code runs against both
- Correcting an answer key rescores history on the server rather than mutating a question that submitted attempts already point at
- Publishing a paper bumps its version and recomputes its checksum, because that is the only signal a phone has to re-download it
- Explicit, generous line height on every Gujarati string: matras extend past the Latin line box and Android clips them by default, which is why it is tested on a real low-cost phone rather than a simulator
Walkthrough
Ninety seconds on a real phone: the papers list, the pre-test briefing, a paper sat in Gujarati with the content language switched mid-question, Option E, the submit confirmation, and the analysis that follows it.










