tryCBT is a local-first UTME practice application for the web and Windows desktop.
The product gives a candidate a focused CBT simulation for Use of English, Biology, Chemistry and Physics, with a 180-question, 120-minute mock flow, answer persistence, subject navigation, keyboard controls, automatic expiry submission, score breakdowns, review and history.
The engineering goal was not to build a flashy quiz screen. The goal was to make the core exam loop trustworthy even when the browser refreshes, the application restarts, the network disappears, or the user returns to an unfinished attempt.
The product constraint
The first version needed to work for a practical study workflow:
- no required account;
- no backend dependency during an attempt;
- usable on the web;
- packageable for Windows desktop;
- resilient to refresh and restart;
- honest about content provenance;
- testable outside the UI.
That pushed the architecture toward one local-first application instead of a backend-heavy system.
Architecture
tryCBT is one React and TypeScript application distributed in two forms:
textReact + TypeScript application -> installable browser PWA -> Tauri desktop application Local persistence -> IndexedDB through Dexie
The web build is deployed as a Cloudflare Worker with Static Assets. The desktop build uses Tauri 2 to package the same application inside a native shell.
The important boundary is that the exam engine is not coupled to React, Dexie or Tauri. Timer rules, answering, navigation, submission and scoring live in pure TypeScript domain code.
Domain model
The application models the exam flow as explicit domain state rather than loose UI state.
Core concepts include:
- subjects: Use of English, Biology, Chemistry and Physics;
- branded question and attempt IDs;
- candidate profile;
- attempt mode and status;
- per-subject question allocation;
- per-subject navigation position;
- answer snapshots;
- started and expiry timestamps;
- score breakdowns.
The attempt lifecycle is intentionally small:
textnot_started -> in_progress -> submitted -> expired
Only an in_progress attempt can be changed. Once an attempt is submitted or expired, the engine rejects further mutation. That protects the result state from late UI interactions and accidental rewrites.
Durable timer design
CBT timer correctness is one of the places where simple implementations fail.
A decrementing interval is not reliable enough because browsers and WebViews can delay or pause timers when a tab is hidden, a device sleeps, or an app is suspended.
tryCBT stores startedAt and expiresAt on the attempt. Remaining time is derived from the current timestamp:
textremaining = max(0, expiresAt - now)
The interval is only responsible for refreshing the display. It is not the clock of record.
This makes the timer resilient to refreshes, delayed intervals and application restarts. If the restored attempt has already passed its expiry, the application can submit it as expired instead of accidentally granting extra time.
Local persistence
The first schema uses two IndexedDB tables:
profilesattempts
Each answer selection writes the updated attempt snapshot immediately through Dexie. Flags, navigation state and terminal submission state are also persisted.
On startup, the app loads the candidate profile and searches for an in-progress attempt. If one exists, the dashboard can resume it with the previous answer state and the correct remaining time.
The schema is deliberately simple because the initial product is a single-device study tool. Normalizing answers or adding cross-device sync can come later if the product requirements justify the extra migration and privacy work.
Content validation and provenance
The starter bank contains original sample questions and repeated practice variations. It validates the complete product flow, but it is not presented as a licensed comprehensive past-question archive.
That distinction matters.
The question-bank format includes:
- unique question IDs;
- subject;
- year or
null; - topic and subtopic;
- stem;
- four options with A-D keys;
- correct option;
- explanation;
- difficulty;
- source type;
- optional passage references.
The validator rejects malformed questions, duplicate IDs, incomplete option keys, invalid correct answers and missing passage references.
For an educational product, this is not just a data-quality concern. It is a trust boundary. The application should not quietly mix uncertain content into an active practice bank.
Web and desktop delivery
The same Vite build powers both distributions.
For the browser version, PWA support precaches the application shell and bundled assets so the app can continue to work after the first successful load. Candidate data remains in IndexedDB.
For desktop, Tauri packages the same application into a Windows installable form while keeping native code minimal. The first checkpoint release included Windows MSI and NSIS installer generation through GitHub Actions.
The result is one product surface with two delivery paths, rather than two separate applications drifting apart.
Verification
The first release was checked through:
- TypeScript compilation;
- linting;
- unit tests for the exam engine;
- IndexedDB persistence tests;
- question-bank validation over 180 records;
- browser-flow tests with Playwright;
- production deployment verification on Cloudflare.
The browser-flow test covers a realistic candidate path: setup, instructions, starting a mock, keyboard answering, navigation, refresh, resume, subject switching, submission confirmation, final submission and review.
That matters because CBT software is mostly state. The dangerous failures are rarely in the static screen. They are in what happens after the user moves through the flow and the environment changes underneath them.
Engineering tradeoffs
The local-first design intentionally avoids several things in the first version:
- no account system;
- no cloud sync;
- no server-side analytics;
- no shared attempt history across devices;
- no licensed comprehensive question archive bundled by default.
Those are not missing because they were forgotten. They are deferred because each one adds operational, privacy, content-rights and support responsibilities.
The first release focuses on the part that has to be correct before the product deserves more complexity: the exam attempt itself.
Outcome
tryCBT demonstrates a practical education product built with senior engineering boundaries:
- deterministic exam lifecycle;
- durable timekeeping;
- immediate local persistence;
- explicit content validation;
- offline-capable web delivery;
- desktop packaging from the same codebase;
- test coverage around the state transitions that matter.
The project is useful because it respects the workflow. A candidate does not care that the stack is elegant. They care that the mock starts, answers persist, the timer does not lie, submission is clear, and review works after the exam ends.
That is the standard the first version was built around.