work / preppals
PrepPals
CAPS exam practice that feels like a game.
A free practice platform for South African learners in Grades 5 to 12. Short visual lessons, exam-style questions with instant feedback, spaced review, and progress that parents and teachers can follow. One person, every layer, running on Cloudflare's edge.
- status
- live
- started
- November 2025
- domain
- EdTech · South Africa (CAPS curriculum)
- role
- Everything. Product, curriculum pipeline, design, code, infrastructure, operations.
- layers
- Product · Content · Interface · Application · Data · Edge & network · Operations · Security
- stack
- Next.js 16 / React 19OpenNext on Cloudflare WorkersD1 (SQLite)R2VectorizeWorkers Rate LimitingCron TriggersGemini (generation, embeddings, TTS)Tailwind CSS 4KaTeX · Mafs · three.jsTypeScript · node:test
- 8grades, 5 to 12
- 25subjects
- 974CAPS topics
- 8 055curated questions
- ~17 000AI-pool questions, quality-scored
- 697commits in year one
- 326test files
- 22D1 migrations
architecture
screens




The problem
South African learners write exams set against CAPS, the national curriculum: a precise grid of grade, subject, term and topic. Good practice material exists, but it is scattered across PDFs, paywalled, or generic “maths practice” that does not match what was taught in class this term. And it needs to run well on whatever phone is to hand.
PrepPals is one answer to that: open it in a browser, pick your grade, and practise exactly the topics your class is on, with an explanation after every question whether you got it right or not.
What it does
- Learn, practise, see progress. Each topic opens with a few short slides (the rule, a worked example, a common mistake), then exam-style questions with instant feedback, then a mastery level per topic and an exam-readiness view.
- Spaced review. Wrong answers come back tomorrow; right ones come back later and later. A Leitner-style box schedule, keyed on SAST day numbers so “tomorrow” means a learner’s tomorrow.
- No email for kids. Learners sign up with a nickname, get a login code and choose a PIN. Parents and teachers sign in with an email magic link.
- Families, classes and schools. Parents link a child with the child’s code and PIN. Teachers are verified with their SACE registration number and their school, then create classes with join codes, set an exam scope and date, and see progress by nickname.
- Teacher moderation. Verified teachers in subject-and-phase groups propose fixes to questions, discuss them, and approve them, with the owner approving every change until a group is large enough to trust itself.
How it is built
The whole thing is a Next.js app compiled with OpenNext into a single Cloudflare Worker. There are no servers to patch, no containers, and the bill at this scale is close to nothing, which matters for a free product.
Data lives in D1, Cloudflare’s SQLite. Learners, attempts, review schedules, classes, question versions and the moderation audit trail are all rows there, evolved through 22 hand-applied migrations. R2 caches generated speech, so every read-aloud prompt is synthesised once and then served as an immutable MP3 forever. Vectorize holds embeddings of every pooled question.
A nightly cron (03:00 UTC) tops up thin topics: it asks Gemini for new exam-style items, embeds them, rejects anything too close to an existing question in Vectorize, quality-scores what is left, and runs item analysis on the answers learners actually gave. Questions that perform badly lose their place in rotation without anyone having to notice by hand.
Decisions worth writing down
The server marks the answer
The first version believed the isCorrect flag the browser sent. One edited request could inflate XP, mastery, and everything a
parent sees in their weekly email. Now the server re-marks every multiple-choice answer against the exact question version the
learner was shown, falls back across approved versions if an edit landed mid-session, caps the claimed streak at the run of answers
it has actually seen, and stamps every attempt with marked_by so the share of progress that still rests on the browser’s word is a
single SQL query away.
Question identity has to survive an edit
Spaced review originally keyed a question on a hash of its prompt. That is stable until somebody fixes a typo, at which point every learner’s schedule for that question is silently orphaned. Moderation was impossible until that was fixed, so questions got stable ids and immutable versions, attempts record the version shown, and edits create a new version and move a pointer, atomically.
Living inside the free tier, on purpose
Moving roughly 200 000 rows of question data into versioned tables would blow through D1’s free daily write limit in an afternoon, and hitting the hard limit stops the whole app from querying. So the migration is planned as about six nights at 40 000 rows, always under a self-imposed 60 000 ceiling, with a backup before every phase and an explicit stop rule. Boring, deliberate, and nothing breaks for learners.
A whole class behind one IP
Rate limits are set at the edge with the Workers Rate Limiting binding, and sized for how the product is really used: a school lab is thirty learners behind one NAT address, so the per-IP limit is generous (120 calls per 10 s per route), a signed-in learner gets their own budget, and the code fails open if the binding is missing. Never drop a fast learner’s answer.
Children’s privacy is a hard boundary
The learner, parent and teacher areas never load ads or third-party analytics. A guard test walks every private route to keep it that way. Public information pages may show non-personalised ads, and only after the visitor accepts, behind Consent Mode v2 with reserved space so nothing shifts. A teacher sees a nickname and progress, never more.
What I would tell someone starting the same thing
Write the decision down when you make it (there are ADRs and maintainer notes for every sharp edge), test the boring invariants (stable keys, private routes, migration order), and measure content quality like you measure uptime. In an education product a wrong answer key is an outage, even if every server is green.