Blocknauts LMS
A learning platform that replaced spreadsheets and WhatsApp admin for a free blockchain bootcamp.
Academy
Leaderboard
ReaderContext
- Role
- Builder: product, design, database design, front end, deployment
- Team
- Solo build for The Blockchain Society, ALCHE
- Duration
- 2026
- Stack
- React 19, TypeScript, Vite, Tailwind v4, Supabase, Sanity, Vercel
- Status
- Live; proprietary repository
Blocknauts is the free eight-week blockchain bootcamp run by The Blockchain Society at The African Leadership College of Higher Education in Mauritius. More than 200 people registered. Seventy became active learners, 22 graduated, and the cohort reached four countries.
The platform is the Academy: a course catalog, a 172-page reader, quizzes, XP, a leaderboard and fourteen interactive simulations. The bootcamp began on spreadsheets and WhatsApp. The platform replaced that administration with state the learner and the facilitator can both see.
At seventy learners, administration becomes a database problem.
A spreadsheet can hold a list of names. It cannot hold where each person stopped reading, which quiz they failed twice, what they earned for a completed module, and which of those facts changed after a lesson was edited. Those answers lived in different messages, and someone had to reconcile them by hand.
Progress is a promise
A learner expects to close the tab, open the course on a phone, and continue at the same page with the same XP. That is a promise about state, and it has to survive a refresh, a new device and a content edit. The course is long: 172 pages across 34 modules, with 29 quizzes and 247 answer keys.
A leaderboard is a claim about every learner
XP makes the course feel like progress. It also turns the browser into an attacker. If a learner can write a score directly to the database, the ranking stops meaning anything. If a learner can read the answer key, the quiz stops meaning anything. The trust boundary is the product.
A leaderboard is only worth the write path behind it.
The catalog keeps moving
Lessons are edited by people who should not need a deploy. Progress is keyed to page IDs, so a renamed page or a deleted module can orphan a learner's history or push a progress bar past 100%. The content system and the progress system needed one shared contract.
Approach
Content is a contract, not a page
Lessons are authored in Sanity and stored with stable identifiers: a course ID, part ID, module ID and page ID. The original TypeScript course files remain the canonical fallback and seed source. Publishing a change fires a webhook that re-syncs the Postgres catalog, so the app that reads progress and the app that renders a lesson agree on the same IDs.
The reader counts progress against pages that still exist. Stale IDs are ignored, a saved quiz index is clamped to the current question list, and a percentage cannot exceed 100. A content edit becomes an ordinary event.
The browser proposes; Postgres decides
Every trusted write goes through a SECURITY DEFINER function. The tables have no direct insert, update or delete policies for learners. The browser can ask to complete a page. It cannot name its own XP, mark its own answer correct, or set its own role.
| complete_page | Checks the published page, the welcome video, the previous page and quiz completion before awarding XP |
|---|---|
| submit_quiz_answer | Compares the selection against a server-owned key and records the attempt |
| finish_quiz | Requires a correct attempt for every question before it marks the quiz finished |
| save_quiz_state | Stores the question index and attempts; finished is owned by finish_quiz |
| update_profile | Validates display name and country; role and tester status stay out of reach |
XP is an audit log
XP is not a number the app increments. Every award is a row in xp_eventswith a unique constraint on user, course, source type and source. A repeat completion adds nothing. The profile total is recalculated from the event log, and tester accounts are forced to zero and excluded from the public view. The leaderboard reads validated totals, never a client-supplied score.
Threats, written down and tested
The security spec lists sixteen attack payloads before the policies exist. Each one maps to a database rule.
| Self-promotion | No update policy for role or tester status; profile writes go through update_profile |
|---|---|
| XP injection | No update policy for xp; totals come from xp_events |
| Answer-key scrape | No select grant on quiz_questions for learners; admin-only policy |
| Correctness forgery | submit_quiz_answer computes correctness from the stored key |
| Sequence skip | complete_page checks the welcome gate and the preceding published page |
| Identity theft | Progress policies compare user_id to auth.uid(); writes come from the RPC |
A migration, not a reset
The bootcamp already had learners on Firebase. Moving to Supabase meant carrying 81 people, their completed pages, module progress and XP into the new schema. The import script maps old page IDs onto the catalog, writes an audit event for migrated XP, and skips IDs that no longer exist. The migration guards are idempotent, so replaying it on a cloned branch is safe, and legacy progress bypasses the new ordering rules instead of blocking a returning learner.
Shipped
The Academy hub
A signed-in home for the course: current progress, the next lesson, XP and the catalog. The capture shows 24 of 172 pages complete, 240 XP and 14% progress, which is the honest shape of an eight-week course in its first weeks.

The reader
A lesson view with the module outline, a progress rail, resources, a glossary and the XP total in the header. Pages unlock in order. The welcome video gates the course, and a quiz page stays locked until the quiz has been passed. Video, text, interactive and quiz pages share one reader, so the learner keeps one sense of place.

Quizzes that keep their answers
A quiz shows one question at a time, checks the answer on the server and lets the learner retry. Finishing requires a correct attempt for every question. The saved index survives a refresh; if the question list changes, the app clamps the index instead of crashing. Twenty-nine quizzes and 247 answer keys ship without the keys reaching the browser.
The leaderboard
Seventy ranked learners, country and validated XP. The view excludes testers and never exposes an email address. The screenshot is blurred because the names belong to real learners.

Fourteen simulations
Hashing, block structure, chain integrity, network topology, two consensus models, token supply, NFT metadata, bridge flow, escrow, transaction lifecycle, incentive design, CEX versus DEX, and career paths. Each one is a small interactive model of the concept in the lesson beside it, built to run without a backend.
The public catalog is intentionally thin while the bootcamp runs as a single cohort course. The platform supports more courses; the society has not published them yet.
Evidence
10 tests pass, 16 skipped, 0 fail. The skipped suite is the Supabase security suite, which needs a live instance. Typecheck and production build pass; the build takes 1 m 46 s.
81 learners migrated from Firebase with their completed pages, module progress and XP preserved.
Reflection
The reader took the most screens. The write boundary took the most thought. A course platform is a set of promises about state, and the interface is only the visible part of keeping them.
Writing the threat model before the policies changed the schema. Once the attacks were named, the answer was usually to remove a permission and add a function. The database became the specification, and the client became a caller.
The next version would give facilitators a cohort view: who is stuck, which question is failing, who is ready to graduate. That data already exists in the tables. It has no screen yet. I would also move the security suite into CI against a disposable Supabase branch, so the sixteen checks that skipped locally run on every push.
Make the state true, then make it visible.