Web AppQ4 2021Solo build
QuizTown
High-end quiz platform that saves progress and rates the user at every level.

The build
Nine categories, ten levels each, and a rating waiting at the end of every one.
Problem
Most quiz sites forget you the moment you close the tab. That's fine for a single trivia round and useless for anything with progression: without persistence there's no reason to come back, and without a rating there's no reason to replay a level you already passed.
Constraints
- Ninety levels of content. Nine categories, ten levels each. The data model had to make adding a level cheap, because most of the work was going to be content, not code.
- Progress has to survive a lost session. Anonymous play with
localStoragewas the easy version and the wrong one. - Two identity providers. Facebook and Google, because the audience had one or the other, rarely both.
Approach
Levels are documents, not code. A category holds ten of them; each level holds its questions and its scoring rule. Adding content is a database write, not a deploy.
Progress is stored per account per level, so the state that matters — furthest level reached, rating achieved — is a small document rather than a recomputation over an answer log:
// One document per player per level. Upserted on completion.
await Progress.updateOne(
{ user: userId, category, level },
{ $max: { rating }, $set: { completedAt: new Date() } },
{ upsert: true }
);$max is the whole replay rule: finishing a level again can raise your rating and can never lower it.
Tradeoffs
- Storing a rating rather than the answers. Compact and fast to read, and it throws away the data that would have let me analyse which questions were badly written. A log would have been bigger and more useful later.
- Two OAuth providers instead of one. Doubled the auth surface — two apps to register, two callback flows to keep working — for coverage of an audience that wouldn't sign up otherwise.
- Content in the database over content in files. Adding a level got easy; version control over the questions got lost. Today I'd put them in files and validate at build, which is precisely what this portfolio does with MDX.
Outcome
The full ninety levels shipped, with progress and ratings intact across sessions and both sign-in paths working. It's the earliest project of mine that had real state to protect, and the first time getting persistence wrong would have been visible to a user rather than just to me.
Decisions
- Accounts over anonymous play. Chosen because progression is the product, and progression without identity is a cookie waiting to be cleared. Against it: a sign-in wall before the first question is the highest-friction thing you can put in front of a casual player.
$maxon rating over storing every attempt. Chosen for a read path that never has to aggregate. Against it: no history, so improvement over time is invisible.- Materialize CSS over writing my own. Chosen to spend the time on the level system instead of on components. Against it: the site looks like every other Materialize site, which is a real cost on a portfolio piece.
Stack
- Node.js
- Express.js
- MongoDB
- Passport.js
- EJS
- Materialize CSS
Related projects

Lecture Lense
Turns any recording into searchable, timestamp-cited notes.

Chitter
A mini social app that lets users sign up, log in and post whatever is on their mind.

Heckfree
Users get a public profile that showcases all of their links in one place.