← All projects

rallyready/

Self-directed badminton training for a player with no coach and no partner. The app is the random caller. Every corner is spoken out loud, so a whole session can go by without you looking at the screen once.

role
Sole developer. Product, interface, data model, tests
stack
React 19 · TypeScript · Vite · Supabase · Vitest
started
August 2026
status
Live on Vercel
The Train screen. You pick an intent, the app picks the drill.
The Train screen. You pick an intent, the app picks the drill.
Twenty-eight seconds of a real four-corner drill: the countdown, the warm-up, then Round 1, where each called corner lights up on the court and the count of calls answered climbs. It is silent. It was recorded in a browser with no voice installed, so the Taglish calls you would hear on a phone are not in it.
The problem

Training alone means nobody is shouting a corner

Badminton footwork gets trained by someone shouting a corner at you. Alone, there is nobody to shout, no partner to feed, and often no court. What is left is a dozen YouTube tabs and no record of whether any of it worked.

The build

Audio first, and load instead of minutes

Programs: periodised plans that decide what to train and when.
Programs: periodised plans that decide what to train and when.
The call

A coach in Manila does not shout in English

Most of the players this is for are Filipino, and it called every corner in English. On a court in Manila a coach shouts likod kaliwa, and then, in the same breath, smash. Taglish is not a translation of the call. It is the call.

So the vocabulary is split where players split it. Corners, zone numbers and phase words are Filipino. Shot names and exercises stay English, because nobody has ever shouted a Tagalog word for a smash, and push up is what is said in a Filipino gym. The interface stays English too: it is read sitting down, with time to think, which is the opposite of a call.

Every phrase is written twice. Android usually ships a fil-PH voice and can say the real word; where there is none, the call is respelled so an English voice still produces something a player can move to. Which voice reads kah-lee-wah is the difference between an instruction and a noise, so the voice is now a per-language setting. It had been stored since launch and never read: the function that would have applied it had no callers, so every session the app ever spoke used whatever the browser picked.

Engineering

648 tests, and the parts that decide how hard you train

There are 648 tests under Vitest, and npm run verify (typecheck, lint, tests, build) is what CI runs on every push. The drill engine, the load model, the program generator and the benchmark scoring are all pure functions with tests behind them, because those are the parts deciding how hard somebody trains.

It installs as a PWA. The timer, the drills and the coaching notes work with no connection at all, and sessions still log. The accounts layer behind that is built and tested rather than live: Supabase auth, a full repository implementation and local-to-account migration all exist, and the app ships local-only until the owner adds two keys. The README says so plainly, because a tester deserves to know that before investing a month in it.

The original brief asked for curated third-party video and I left it out. Embedding other people's coaching clips means shipping a library I do not own and cannot keep alive. The reference library is written instead. I wrote that decision down in the repo rather than quietly dropping the feature.
Six bugs

Green tests, and an app still wrong in the hand

The tests were green and the app was still wrong in ways only using it on a phone would show. Six of them, found by treating it like a real session rather than a test run.

Verified across 65 route, viewport and theme combinations, the full first-run path, an eyes-free drill run to completion, and offline, corrupt-storage, back-button and rapid-navigation passes. The lesson I keep from it: a green suite tells you the code does what you wrote down, not that the app works in a hand.
What a QA engineer found

Two findings, and the first was worse than he could see

The app went out to club players, one of whom works in QA. His first point was that it only saves in the browser: clear your cache or change phone and a month of training is gone. The accounts layer was already written and merely switched off, so the answer looked like pasting in two keys.

It was not. supabase/schema.sql did not run at all. Two alter type ... add value statements had been pasted into the middle of its enum block, splitting the block and orphaning an end if;, so Postgres rejected the file on its first statement. Anybody following the file's own instructions would have got an error and no tables. The feature he asked for could not have been switched on even with the keys in place.

A broken schema also hides its own drift, and there was some: one enum had lost track of a value added two phases earlier, and another never gained two more. Applied to a real PostgreSQL 16 after the repair, twice from empty: 12 tables, every one with row-level security, 41 policies, 11 enum types, 30 seeded drills, no errors on either run. A test now checks the structure and that every enum value the app uses is declared, and it was verified by putting the original defect back and watching it fail.

His second point was that the app read as too technical for players who are not developers. That is an opinion until it is measured, so it was measured, on the text the browser actually rendered, across fifteen screens.

Sentences longer than twenty words

25 to 0

Longest sentence in the app

38 to 19 words

Total words a player has to read

4,501 to 3,867

Reading grade, same information

4.2 to 3.6

Fourteen per cent fewer words for the same content. Deload is now Easy week; Effort × minutes is how hard you trained, not just how long. What did not change is the specificity: no payments are connected yet is still there and still blunt, only shorter. Simpler was not allowed to mean vaguer, and a test holds that line, capping every drill and program string at twenty words a sentence.

Why it matters

The most complete thing I have built

It is the most complete thing I have built. Five phases, a real domain model, an offline story, an audio pipeline, and enough tests that I can change the program generator on a Tuesday without being afraid of it.