Blocknauts LMS

A learning platform that replaced spreadsheets and WhatsApp admin for a free blockchain bootcamp.

2026Live productPrivate repo
Blocknauts Academy hub with course progressAcademy
Blocknauts leaderboard with learner names blurredLeaderboard
Blocknauts lesson reader with module index and contentReader

Context

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_pageChecks the published page, the welcome video, the previous page and quiz completion before awarding XP
submit_quiz_answerCompares the selection against a server-owned key and records the attempt
finish_quizRequires a correct attempt for every question before it marks the quiz finished
save_quiz_stateStores the question index and attempts; finished is owned by finish_quiz
update_profileValidates 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-promotionNo update policy for role or tester status; profile writes go through update_profile
XP injectionNo update policy for xp; totals come from xp_events
Answer-key scrapeNo select grant on quiz_questions for learners; admin-only policy
Correctness forgerysubmit_quiz_answer computes correctness from the stored key
Sequence skipcomplete_page checks the welcome gate and the preceding published page
Identity theftProgress 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.

Blocknauts system map: browser, Supabase Auth, trusted RPC boundary, Sanity content sync, Postgres state and leaderboard view
The trust boundary: the browser reaches trusted state only through security-definer functions. Content syncs from Sanity into the same Postgres catalog that validates progress.

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.

Blocknauts Academy hub showing course progress, XP and the course catalog
The Academy hub: progress, XP and the next lesson in one place.

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.

Blocknauts lesson reader with module outline, lesson content and resources
The reader: outline, lesson, resources and glossary. One page type at a time, one place in the course.

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.

Blocknauts leaderboard with learner names blurred
The leaderboard: seventy ranked learners, country and validated XP. Names redacted.

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

200+registrations across four countriesProgramme records, 2026. Seventy learners active; 22 graduated.
172course pages across 34 modules29 quizzes and 247 server-owned answer keys. Counted from the seeded catalog migration.
16row-level security policiesTen tables, two views, four indexes and nine security-definer functions in the core schema.

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.
NextAniLearn