Skip to content

Building in Public: Modernizing schlau.app

A daily protocol of reviving and rebuilding a free math practice platform for grades 7–10 — for teachers curious about what it actually takes to build (and gamify) an ed-tech tool, and for students interested in how apps like this get made.


Day 1 — Waking up a 3-year-old codebase

schlau.app has been quietly helping students practice math for years — but the code behind it hadn't been touched in three years. Today was about getting back into it, safely, with an AI coding assistant (Claude Code) as a pair-programming partner.

What happened

  • Installed Claude Code, hit a Node.js version mismatch, fixed it with the native installer instead of fighting version managers
  • First move: had Claude Code read the entire codebase and explain it back before touching a single line — turned out the app runs on Create React App with zero backend, every math problem generated client-side
  • Found a subtle landmine early: the live production branch (master) wasn't even the one GitHub called "main" — a naming trap that could've caused real confusion later
  • Set up a strict rule for the whole project: all work happens on a separate branch, the live site is never touched until we're ready
  • Wrote a CLAUDE.md file — a standing brief the AI reads every session, so it doesn't have to re-learn the architecture from scratch each time
  • Refactored how math-problem generators return their data (from fragile positional arrays like [a, b, c, d] to named objects) — a "boring" but essential cleanup that makes every future feature safer to build
  • Introduced a small registry system so adding a new type of math problem in the future means touching one file instead of three
  • Started the actual migration to Next.js — got a minimal working shell running side-by-side with the old app, no functionality lost

Lesson for the audience

The least exciting work (reading code, fixing dependency versions, writing documentation for the AI) is what makes everything after it fast and safe. Nobody sees this in the final product, but skipping it is how side projects die.


Day 2 — Real routing, retiring the old stack, going live

With the foundation solid, today was about finishing the migration and getting something real deployed.

What happened

  • Replaced the old hacky navigation (?atype=lin1 query parameters) with real, clean URLs (/lin1) — meaning every math topic now has its own shareable, bookmarkable page
  • Verified the change three ways before trusting it: automated build checks, an isolated runtime test running 3,000+ simulated task generations, and clicking through the actual app in a browser
  • Switched the whole app to a static export, retiring the old build system (react-scripts) entirely — and along the way, an automated linter caught two real pre-existing bugs nobody had noticed
  • Cleaned up dead weight: removed three unused dependencies that had been silently shipped in every build for years
  • Deployed the new version to Vercel (the company behind Next.js) — while the old live site kept running completely undisturbed on the original hosting the entire time
  • Hit a genuinely confusing UI puzzle getting the right branch to actually deploy — a reminder that even "just deploying" isn't always one click

Lesson for the audience

Modernizing an app doesn't mean breaking the one people already use. The whole session ran in parallel with the live site, completely isolated, until the new version was proven to work.


Day 3 — Killing a fragile dependency, starting real accounts

Two more pieces landed today: a UX cleanup that had been bugging the developer for years, and the first real steps toward user accounts — the foundation gamification will eventually sit on.

What happened

  • One part of the app (sub-topic filters for four question types) had always relied on a fiddly third-party dropdown component that was never fully documented and quietly had bugs — including one question type where the filter had been silently broken the entire time
  • Replaced it with native, framework-level routing — same UX, zero extra dependencies, and the hidden bug got fixed as a side effect
  • Verified the fix by generating real math problems in a build check and clicking through in a live browser
  • Then pivoted to the next big milestone: user accounts. Chose Supabase (a Postgres database with built-in authentication) as the backend, and social login (Google, more providers to follow) so students never have to manage a password
  • This required one more architectural shift — moving from a fully static site to a server-rendered one, since real authentication needs a server to verify sessions securely (a deliberate tradeoff, made with eyes open)
  • Got Google sign-in fully wired end-to-end: OAuth credentials, consent screen, and a working connection between Google, Supabase, and the app

Lesson for the audience

The fun part — points, streaks, leaderboards — needs a boring prerequisite first: a database and a login system people can trust. That groundwork went in today.


What's next

The next phase is the one gamification-minded readers will care about most: a profiles table for every user, and from there, points, streaks, and leaderboards — the Duolingo/Khan-Academy-style layer that turns solo practice into something more like a game.

Day 4 — Auth, hardened

Today's session picked up right where Day 3 left off: Google sign-in was working, but the second piece — a way in for students without a Google account — turned into a much bigger day than expected.

What happened

  • Kicked off the day meaning to add Microsoft as a second login option, alongside Google. Hit a wall almost immediately: the only Microsoft tenant available was the school's own, with no admin rights and an expiration date already looming. Tried the official "get your own free developer tenant" signup path — Microsoft's automated eligibility check rejected it outright, a known, fairly common snag with no reliable manual override
  • After a second attempt still failed, made a clean call: drop Microsoft entirely. Too much account complexity for a secondary login option, especially with only debit cards on hand for what Microsoft's flow expected. Google plus a passwordless option would cover it instead
  • Landed on magic links — email-only sign-in, no password, no second OAuth provider to wrangle. Supabase handles the token generation and verification natively
  • Building it surfaced a real, non-obvious security issue: magic links can get silently burned by email providers before a student ever clicks them. Gmail and school email gateways often pre-visit links in incoming mail to scan them for safety — and since a magic-link token is single-use, that automated pre-visit consumes it, so by the time the real click happens, the link is already dead. Fixed it properly rather than shrugging it off: built a custom confirmation page that requires an actual button click before the token gets used, immune to automated pre-visits
  • That fix cascaded into more plumbing than expected: Supabase, it turns out, now requires a custom SMTP provider configured before its email templates can even be edited — the built-in mail sending is locked to defaults. Set up Resend as the SMTP provider, verified the domain via DNS (MX, SPF, DKIM, DMARC records), and rebuilt the email templates to point at the new confirmation page
  • Along the way, a genuinely unnerving incident: two database tables that had seemingly migrated successfully vanished without explanation. No conclusive root cause was ever found (a downside of being on a free-tier project with no backup history to inspect) — but it led to a concrete process fix: from now on, every migration gets verified by directly querying for the table's existence, not just trusting a "Success" message

Lesson for the audience

The unglamorous failure mode of email-based login — a link getting "clicked" by a robot instead of a human — is exactly the kind of thing that's invisible until it silently breaks sign-in for a chunk of real users, often the ones on the most locked-down school email systems. Worth building it right the first time, even when it means several extra hours of plumbing on what looked like a simple feature.


What's next

With both login paths solid, the next stretch of work turns to the fun part readers have been waiting for: a profiles table, a points-and-badges system, and the first steps toward a leaderboard.

Day 5 — The gamification foundation: profiles, progress, and a plan on paper

With login solid, today was about building the invisible layer everything else would eventually sit on — before writing a line of reward-system code, the design itself had to be pinned down.

What happened

  • Designed the whole reward system on paper first: three tiers of icons — activity icons that light up on any practice click at fixed lifetime thresholds, topic icons (one per math subtype, so every kind of problem gets its own visible progress), and bonus icons tied to specific behaviors like using help or checking a result
  • Built the actual schema: a profiles table, a daily_progress table, and a single increment_daily_progress upsert function that every future write would route through — one entry point instead of scattered inserts
  • On the client side, wrote an accumulator that batches writes rather than hitting the database on every single click — flushing roughly every 10 actions or 20 seconds, and seeded from a student's existing lifetime totals at the start of each session so nothing resets by accident
  • None of this was visible in the app yet. Today was entirely plumbing.

Lesson for the audience

The badge lighting up on screen is the fun part, but it can't be trusted until the thing counting it is trustworthy first. A reward system's foundation is invisible by design — the whole point is that nobody should ever notice it, even when it's the most important part of the day's work.


Day 6 — Making progress visible, and catching a bug before students would

Today the first genuinely visible piece of the reward system shipped: a badges page students could actually look at.

What happened

  • Built "Meine Abzeichen" — a page showing every tier a student has reached, from a wobbly first-attempt icon up to the highest badge earned so far
  • Found a real bug in review, not from a bug report: a tiered category was showing multiple rows instead of just the highest one reached — a lower tier stuck at its old count sitting right next to a newer, growing one, as if the same progress was being split in two
  • Traced it to the actual cause: each click was being written to whichever tier happened to be active at that moment, rather than one true running total. Fixed it by extracting a shared "which tier, and what's the real cumulative count" rule and applying it consistently everywhere tiers appear
  • Re-verified against the exact numbers that had exposed the bug, plus a few edge cases (nobody's reached any tier yet, everybody's reached the top tier)

Lesson for the audience

A rewards screen showing two conflicting numbers does more damage to trust than an ugly icon ever could. Correctness came before polish, on purpose.


Day 7 — Guarding the game: anti-spam cooldowns

A reward system with no friction against gaming it isn't really a reward system — it's just a counter anyone can inflate for free. Today was about closing that gap before it became a habit.

What happened

  • Added tiered cooldowns tuned to how each button actually gets used: checking a result triggers a soft dim on the "add task" button that escalates to a harder lock if broken early, help and result-check both got their own short self-close cooldowns, and a separate, gentler throttle specifically stops reward-only spam-clicking on the "add task" button — without ever blocking a student from simply generating more practice problems
  • Deliberately kept task generation itself completely unthrottled — the friction only applies to earning, never to practicing

Lesson for the audience

Every reward mechanic needs its abuse case designed alongside it, not patched in afterward once someone finds the shortcut. The fix here wasn't "block spamming" in general — it was figuring out exactly which specific action was worth protecting, and leaving everything else untouched.


What's next

With the core reward loop — earn, see, can't easily cheat — standing on its own, the next stretch turns to something bigger: taking this from a solo experience into a shared one. A classroom leaderboard, visible to a whole class at once, live.


Day 8 — Designing a classroom leaderboard, before touching any code

No code shipped today, on purpose. A feature that puts students' activity next to each other in public needed its hard decisions made deliberately, not discovered halfway through building it.

What happened

  • Decided pseudonyms shown on the leaderboard would rotate daily rather than stay fixed — a stable nickname would eventually become just as identifying as a real name once classmates learn to recognize it
  • Chose to build live updates on top of a lightweight messaging channel rather than the more obvious "watch the database table for changes" approach — the obvious approach would have quietly broadcast raw student activity data to anyone listening; the chosen approach sends a content-free "something changed, go re-fetch" signal instead
  • Settled on sortable, calendar-aligned day/week windows rather than a rolling time range, to match how a teacher actually thinks about a lesson or a week, not an arbitrary trailing period

Lesson for the audience

For anything that puts kids' names — even fake ones — next to numbers where classmates can see them, the privacy decision has to be made before the database schema, not patched onto it afterward.


Day 9 — Classes, join codes, and a security bug caught before it shipped

Today's build finally gave a classroom leaderboard something to belong to: an actual notion of a class.

What happened

  • Built a lightweight class model with a short, teacher-shareable join code — deliberately modeled on the PIN-based join flow from a well-known classroom quiz game, since it's a pattern students already instinctively understand
  • Before any of it went live, a careful review turned up two real security gaps: a new table with no access restrictions at all (anyone could have read or edited invite codes through the public API), and an existing "update your own profile" rule that technically let a signed-in student promote themselves with a single request
  • Fixed both before a single real row of data existed

Lesson for the audience

"It compiles" and "it's safe" are two different bars, and only one of them shows up in a quick demo. Clearing the second one means deliberately asking "what's the worst thing a malicious signed-in user could do here?" — every single time, not just when something feels risky.


Day 10 — The leaderboard goes live, and finds its first exploit

The classroom leaderboard shipped today — and, tested with real accounts, immediately revealed a gap in its own design.

What happened

  • Wired up the actual leaderboard, live and cross-device, verified end-to-end with real test accounts rather than trusted on faith
  • Within a day of testing, a real exploit surfaced: a student could top one leaderboard column just by spam-clicking a single button, with zero actual effort behind it
  • Replaced the raw click-count with a computed score that combines multiple behaviors using a geometric mean — a formula with a genuine mathematical property (diminishing returns from hammering any one input alone), not just a number that's harder to reverse-engineer

Lesson for the audience

Real users find the exploit in a point system faster than expected — that's not a failure, it's the test the system needed. The fix that survives isn't "make it confusing," it's making the underlying math actually resistant to being gamed.


Day 11 — Rebuilding navigation for the device students actually use

A page that looks fine on a laptop can quietly fall apart the moment it's tested on the device most students will actually use.

What happened

  • Testing on a real phone showed nav labels breaking and wrapping at narrow widths — invisible on a laptop screen, immediately obvious on a phone
  • Checked current mobile design guidance before reaching for the default fix: the once-standard "hide everything behind a menu icon" pattern has fallen out of favor for apps with a small, stable set of main destinations, replaced by a row of icons fixed to the bottom of the screen, always visible
  • Rebuilt navigation around that pattern — primary destinations always in thumb reach, secondary actions like signing out tucked into a small account menu instead of competing for space

Lesson for the audience

Most students will never open this on a laptop. Designing for the phone first — and checking current evidence instead of defaulting to a decade-old pattern — was the actual product decision here, not just a technical detail.


Day 12 — Letting a colleague join without needing an admin

The last piece before this can spread beyond one classroom: making sure a real colleague can start using it without ever needing direct database access.

What happened

  • Built a self-serve path for a colleague to become a teacher: redeem an invite code, land straight on a page that lets them create their own class, and immediately get back a shareable join code — no manual setup on the backend required
  • Along the way, closed a subtle bug: promoting someone from student to teacher had been leaving their old class membership sitting in their data, which could quietly confuse the app about which view to show them later. Fixed at the source, not patched around in the interface

Lesson for the audience

The whole point of building this was to hand it to real colleagues without personally being the bottleneck for every single one of them. Today made that actually true, not just true in theory.


What's next

With the reward system, the classroom leaderboard, and self-serve teacher onboarding all standing on their own, the next stretch turns outward: real classrooms testing all of it at once, and the quieter groundwork — multiple teachers, multiple schools, oversight as it grows — that a tool like this eventually needs once more than one school is relying on it.

Day 13 — Two cheats closed in one sitting

Small bugs are still bugs. Today closed two of them — one about clicking too fast, one about leaving too slow.

What happened

  • The "=" button had a gap the anti-spam system missed: mass-clicking it repeatedly could rack up rewards, even though the "+" button already had a throttle for exactly this. Fixed by mirroring that same pattern onto "=" — reward-only, function never blocked, verified not with a stopwatch but by literally counting how many times the result panel toggled across rapid clicks
  • The bigger job: letting a student actually leave a class, and a teacher actually remove one. Sounds trivial. Wasn't. The database itself refused to let anyone touch a class assignment directly — a security rule written months ago to stop students from silently reassigning themselves now stood in the way of the legitimate version of the same action. Fixed properly: three narrow, purpose-built database functions instead of one loosened rule
  • Testing the teacher side turned up a real discrepancy — the leaderboard for a class showed two students, the new member list showed zero. Same class, same data, two different answers. Found and fixed before a single real teacher touched it

Lesson for the audience

The bug that matters isn't the one you find reading code at your desk. It's the one you find clicking through the app like the twelve-year-old who's about to use it.


Day 14 — The badge that wouldn't sit still

Not every fight gets won today. This one didn't — and that's worth saying out loud.

What happened

  • A small layout problem on the badges page — two categories that needed to visually read as two groups, not one — turned into four separate attempts across a single afternoon
  • Round one: merged into one heading. Wrong — lost the distinction entirely
  • Round two: two labels, but no visible boundary between them, and the font didn't match the heading below it
  • Round three: two bordered boxes, correct structure — but now the longer German label physically didn't fit the box
  • Round four: added icons to shrink the text footprint. Still overflowed
  • Called it. Left it as-is, moved to the next item on the list

Lesson for the audience

Not every visual problem is worth the fourth attempt. Some German compound words are just longer than the box you drew for them — and knowing when to stop iterating is its own skill, not a defeat.


Day 15 — Twenty phones, one broken login

The single biggest fix of the whole rebuild didn't touch a database, a leaderboard, or a badge. It touched the one thing every single student has to get through before any of that matters: signing in.

What happened

  • Email magic links had been working fine in solo testing for weeks. Then the actual failure mode became obvious: a magic link opens in a new browser tab or app on most phones — and there's no reliable way back to the original tab afterward. One student, one weird phone, that's an annoyance. Twenty kids in a classroom hitting the same failure mode mid-lesson is a derailed lesson
  • The fix: replace the link entirely with a numeric code, typed into the same window it was requested in. No tab-switching possible if there's no tab to switch to
  • Building it surfaced something invisible until you go looking: Supabase quietly sends two different email templates depending on whether you've signed in before — one for new accounts, one for returning ones. Fixing the visible half of that split and missing the other half means half your students get a working code and the other half get an old-style link with no code on it at all
  • Then a second, even less visible fork: verifying a code from a brand-new account and verifying one from a returning account requires telling Supabase which case you're in — except the app has no way to know in advance which one applies to any given email. Solved by trying the more common case first, and quietly falling back to the other if it fails. The student never sees the fork. They just type six digits and they're in

Lesson for the audience

One login button. Two invisible forks underneath it, neither documented anywhere obvious, both silently broken until someone actually tried logging in as two different kinds of user on a real phone. The feature nobody notices working is usually the one that took the longest to get right.


Day 16 — Redesigning the front door

Every visitor sees this screen before they see anything else the app can do. It hadn't earned that spot yet.

What happened

  • The not-signed-in header was a dark, high-contrast ribbon — the exact same visual language as the colorful task buttons underneath it. Every screen was shouting at the same volume
  • Rebuilt it with its own calm identity: light background, quiet slate text, clearly separated from both the gradient page background and the loud task buttons below
  • Google's sign-in button now uses Google's actual multicolor logo, following their real branding convention — not a generic icon standing in for it
  • The code-request button got a key icon and, after one honest correction, a lighter fill to match — first pass accidentally recreated the same dark-button pattern being escaped from, in a different color

Lesson for the audience

The screen a new user sees before they've decided whether to trust you deserves more restraint than the screen they see once they already do.


Day 17 — The paperwork nobody films, and a documentation site that changed its mind twice

Two unglamorous jobs, back to back: making the legal side real, and giving the project a public home that isn't a chat log.

What happened

  • Drafted a proper Impressum and Datenschutzerklärung under current German law — full legal name, address, and two working contact channels, not just an email address quietly hoping nobody ever needs a faster reply. The harder question wasn't the boilerplate, it was the honest one: minors use this app, and the app itself doesn't collect separate parental consent — it leans entirely on the existing school relationship. Written down plainly instead of hidden behind vague language
  • First attempt at a documentation site: GitBook. Got as far as pasting content in before hitting a wall that has no workaround — GitBook has no way to resolve an image reference pointing at a file sitting on a local hard drive. Every image would need re-attaching by hand, one at a time, across dozens of files
  • Changed course mid-stream: MkDocs instead, deployed to Cloudflare Pages — real markdown files, images that just work, one command to rebuild and redeploy. Cost an afternoon of dead-end setup to discover, but the right call once discovered

Lesson for the audience

Choosing the wrong tool isn't a failure if you notice fast enough to leave it. The expensive version of that mistake is finishing the wrong tool before realizing it was wrong.


Day 18 — Flipping the domain

The last mile of any launch is never the code. It's DNS — and DNS lies to you in extremely quiet ways.

What happened

  • Repointed the live domain away from an old hosting provider entirely, toward the new deployment — a handful of DNS records, each one looking correct, each one silently wrong
  • The site came back completely unreachable. Not a hosting problem — a single missing trailing period on a target address, which a DNS panel silently mangled into a domain that doesn't exist. One character, invisible in a text field, breaks an entire domain
  • Fixed, re-verified, and confirmed reaching the right infrastructure — the app itself just isn't the version being served there yet, deliberately, pending one last deliberate switch

Lesson for the audience

The scariest bugs in this whole rebuild weren't in application code. They were in a text field with no syntax highlighting, no linter, and a single missing character that looks identical to a working one.


What's next

Everything that needed proving has been proven — solo, on real devices, against real accounts, one deliberate step at a time. What's left isn't a feature. It's a decision already made, waiting on a single deliberate action: flip the switch, merge the work of the last weeks into the version everyone actually uses, and let a real classroom find whatever's left to find. Some things — an admin view, a way to handle more than one school, a real growth dashboard — are staying exactly where

Day 19 — The day it actually went live, and three ghosts in the machine

Every rebuild has a day where the code is finished and the only thing left is flipping the switch — and that's usually where the real chaos hides. Today was that day.

What happened

  • Went to merge weeks of migration work into production and hit an immediate wall: the old and new codebases shared zero git history. Not a merge conflict — a genuinely unrelated pair of family trees, because the rebuild had started fresh on Day 1 rather than branching off the old app. The old codebase had already been declared worthless days ago, so the fix was blunt and deliberate: point production straight at the new code, discarding the old lineage entirely rather than pretending they were ever related
  • First attempt at the actual push silently failed halfway — locally everything looked fast-forwarded and done, but the remote never received it. Caught only because every "done" claim got independently re-verified against the actual remote state rather than trusted on report alone. Found, fixed, re-verified — properly, this time, with the exact commit hash confirmed on the server itself
  • Then the real twist: the site still wouldn't go live. Hours of DNS work, a correct push, a green "Ready" deployment — and still nothing. The actual cause: the hosting platform's "production" branch had quietly been pointing at a third, completely different branch this entire time — an untouched, three-year-old placeholder nobody had looked at since the project began. Every deploy all session had been going to the right code on the wrong branch
  • Fixed by bringing that forgotten branch up to date too, and — critically — updating the project's own standing documentation so a future session (human or AI) won't rediscover the same landmine blind
  • A parting DNS gremlin: the documentation subdomain had been pointed at the wrong service entirely, a stray copy-paste from an earlier fix. Caught by eye, corrected in seconds
  • One mobile browser still refuses to load the site cleanly, despite the same fix working everywhere else tested — reinstalled, cache-cleared, protections disabled, one specific redirect removed and confirmed live. Nothing fixed it. Ruled out enough causes to suspect it's sitting outside anyone's direct control — a stale DNS cache on that browser's own private resolver, not a bug in the app at all
  • Considered turning the app into a fully installable, offline-capable PWA — decided against it, for now. The existing performance score was already strong, installability wasn't something students actually needed, and a poorly-tuned service worker is precisely the kind of caching bug that ate half of today already. Skipped deliberately, not by accident

Lesson for the audience

The scariest part of a launch is never the feature you spent three weeks building. It's the branch nobody's looked at in three years, the push that reports success but silently isn't one, and the one browser out of five that just won't behave — none of which show up until the moment you actually try to go live. Verify everything twice, trust nothing that "should have worked," and know when to stop chasing a ghost that isn't yours to catch.


What's next

The app is live. The rest is no longer engineering — it's putting it in front of real classrooms, watching what actually happens when twenty kids with twenty different phones use it at once, and telling the story of how it got here.