Skip to content
Vijay Gojiya
All work

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.

Screens

  • Kasoti home in dark: greeting, the counters for this week, and the four ways into a test
  • Kasoti papers list: each paper with its length, timing, marks, language and her last score on it
  • Kasoti pre-test briefing: marking scheme, the Option E rule and marks by section, before the clock starts
  • Kasoti question language picker: content language is chosen per paper, separately from the interface
  • Kasoti test player: a long Gujarati reasoning stem, set with explicit line height so no matra is clipped
  • Kasoti with Option E selected: not attempting is a real answer, because a blank is penalised like a wrong one
  • Kasoti submit sheet: every question answered, confirmed once before the attempt is sealed
  • Kasoti result: score, negative marks and accuracy, with a callout pricing the questions left blank
  • Kasoti analysis: marks by section, and a speed-against-accuracy plot drawn against her own medians
  • Kasoti analysis: where marks were lost, where the time went, and what Option E would have changed
  • Kasoti question-by-question review in Gujarati, filtered by wrong, skipped, marked or slow