Rebuilding TopQbank: why we chose greenfield TypeScript over modernizing
The architecture decision behind replacing a legacy Angular and .NET exam-prep platform, and what the rebuild changed for the business.
TopQbank is an exam-prep platform for doctors sitting licensing exams such as PLAB, USMLE, AMC, MCCQE and MRCP. When we took it on, it ran on an Angular front end and a .NET back end that had grown for years without a target architecture. It worked, but every change was slow, and one design decision was quietly costing the business money.
This is how we decided to rebuild it rather than modernize it, and what we would do the same way again.
The decision we wrote down first
Our first architecture decision record proposed the safe option: keep the Angular and .NET code, upgrade it, and fix the worst parts in place. We wrote it, reviewed it with the owner, and then superseded it with a second record that proposed a full greenfield rebuild in TypeScript.
Three things tipped the decision:
- The data model had a bug baked into it. The live site locked exam-type selection into account creation, so one account could not hold subscriptions to two exams. A doctor preparing for PLAB and then MRCP had to create a second account. That is not a bug you patch; it lives in the registration flow, the database and the billing logic.
- The admin team depended on developers. Adding a new exam, category or topic meant a code change and a deploy. The business wanted to launch new exams without waiting for anyone.
- Upgrading two stacks costs about as much as building one. Modernizing meant upgrading Angular, upgrading .NET and keeping both teams’ knowledge alive. A single TypeScript codebase meant one language from database to browser.
Write the decision down before you write the code. The record is what lets you change your mind cheaply.
The architecture
We kept the stack deliberately boring and well supported:
| Layer | Choice | Why |
|---|---|---|
| Web | Next.js (App Router) | Marketing pages, the student app and the admin area in one app |
| API | NestJS with tRPC | End-to-end types between API and web, with no hand-written client |
| Data | Aurora PostgreSQL Serverless v2, Drizzle ORM | Relational data for users, subscriptions and exams, scaling down when quiet |
| Content | Sanity | Questions, the exam taxonomy and marketing pages, editable by non-developers |
| Identity | Amazon Cognito with Google sign-in | Managed auth instead of a home-grown user table |
| Hosting | AWS App Runner, CloudFront | Containers without running a cluster |
| Infrastructure | Terraform, GitHub Actions with OIDC | Everything in code, and no long-lived AWS keys in CI |
Two databases, one source of truth each
The most important design choice was the split between Sanity and Postgres. Sanity holds everything people author: questions, explanations, the Country → Exam → Category → Topic taxonomy, and marketing content. Postgres holds everything transactional: users, subscriptions, exam attempts, answers, payments and support tickets.
Postgres also keeps a synced mirror of published Sanity content, updated by webhook. Runtime queries never call the CMS, so exams stay fast and the content API is never exposed to students.
That mirror needs care. We later found taxonomy drift between the two stores, where a topic was renamed in Sanity but the mirror still pointed at the old one, and fixed it with a one-off cleanup plus a webhook receiver that retries on transient errors instead of silently dropping updates.
Fixing the subscription bug by design
In the new model, registration has no exam-type field at all. Access comes only from subscription rows: one user, many subscriptions. A doctor can add MRCP to the same account they used for PLAB.
Money is stored as integer cents everywhere, never floating point, in the subscription, coupon and refund tables.
Rolling out without a big-bang cutover
We built the new platform on fresh infrastructure and served it on new.topqbank.com first. Real users and real content were verified there before the main domain moved over. The old site kept running until the new one had proved itself.
AI, with a ceiling
Once the foundation was stable, we added an AI pipeline that drafts new questions and explanatory images for the content team to review. Nothing an AI writes reaches students without a human approving it in the CMS, and each user’s AI usage has a monthly cap, so a busy month can never produce a surprise bill.
What we would do again
- Write the decision record first, and be willing to supersede it.
- Give authored content and transactional data separate homes, each the single source of truth for its own kind of data.
- Fix structural bugs in the model, not in the UI.
- Launch on a subdomain and cut over only when it’s proven.
- Put cost guardrails on AI from day one.
If you’re weighing a rebuild against a modernization, the trade-offs are usually clearer than they look once someone writes them down. That’s often where we start.