Pushup Quest
A pushup RPG where the phone's camera counts the reps, and nothing it sees leaves the phone.
- Role
- Solo — product, requirements, testing; code AI-assisted
- Year
- 2026
- Status
- Deployed and in use

The problem
I downloaded a pushup RPG for iOS and found it wanted a subscription to count my own pushups. The idea is a good one — every rep is a sword swing, so a set becomes a fight you want to finish — and a browser can now run a pose model on a phone’s GPU. So I built my own: installable, offline, and with no server that ever sees me exercising.
Counting reps from a camera sounds solved until you point one at a real person in a real room. Joints leave the frame. People stand up, lie down, walk to the phone and back. Squats look like pushups to anything measuring height. And a counter you keep score against has one rule above the others: missing a rep is an annoyance, inventing one makes the whole thing pointless.
How it works
Plain HTML, CSS and JavaScript, no build step. The pose model is vendored, so the app talks to one origin, and a Content-Security-Policy means the browser refuses any other.
Your phone, in the browser
Camera
frames, never recorded
Pose model
MediaPipe, WASM + GPU
Rep counter
depth signal + 5 gates
Game
damage, XP, loot
Saved here
localStorage only
Everything above happens on the device. It works offline once installed.
Outside the device — neither receives video, reps or progress
Static host
served the files once
Google usage stats
patched off, CSP blocks it
The host sees ordinary request details when files are downloaded, like any website.
Three decisions worth defending
A returned joint is not a seen joint
The pose model always returns 33 landmarks, including ones it guessed off-screen, often with middling confidence. Sitting at a desk with my wrists below the frame, the guessed wrists jittered across the depth threshold and counted reps. So every joint a rep depends on must be confident and inside the frame, checked on normalized coordinates, and a dropout of more than 0.7 s abandons the rep in progress rather than resuming it with stale data.
From the front, the elbow angle lies
A camera flattens the scene to 2D, and collinear points stay collinear: a straight arm always looks straight, but a bent one can too when it bends along the camera’s line of sight. So the depth signal is the larger of two cues — the elbow angle, and how far the shoulders have dropped toward the wrists against the arm length measured while it was straight. The second cue is what makes a phone propped in front of you work at all.
In a pushup the hands do not move
Squatting in front of the camera lowers the shoulders exactly like a pushup does; no depth threshold can tell them apart. What differs physically is the hands: in a pushup they are planted, in a squat they drop with you. So each hand is anchored at the top of the rep and must stay within a fraction of an arm length — tracked per hand, because at the bottom the far hand often vanishes behind the body, and an average of two hands jumps when one leaves.
Measured, not assumed
6 / 6
side-view pushups counted on replayed public footage — and 6 of 6 again when the whole app ran in Chrome with that video as its camera.
0
false reps across close-ups and talking, burpees, squats in pushup mode, walking out of frame and 40 seconds idle at a desk. Before the gates: 5, 3 and 1.
23
tests that run in about a second with no browser or camera: synthetic poses, six real clips, and stress cases rebuilt from the failures I hit on my own webcam.
0
requests leaving the app's origin across a 75-second live battle, recorded from the network log — after one was found that did.
The measurement was wrong first. The landmark extractor seeked through each video, and headless Chrome does not repaint a seeked <video> — so every early “measurement” was the first frame repeated hundreds of times. A clip that returned no pose for a plainly visible person gave it away. Frames are now split out with ffmpeg and fed as images, the fixtures are committed, and the numbers can be rerun rather than believed.
What only a real webcam found
The synthetic tests passed, and then I did pushups in front of my laptop. Every failure that mattered came from a body, not from the math.
Counting while I sat still. Guessed wrists below the frame crossed the threshold. The fix was not a tighter number but a different question — is this joint actually seen? — and a test that replays a seated person with jittering off-screen wrists for forty seconds and expects zero.
Slow to find me after I lay down. Losing sight of my arms for a moment wiped the calibration, and confidence flicker kept restarting the “hold the top” timer, so it looped until I froze. Short dropouts now keep the calibration and re-lock in a tenth of a second, and posture has to be wrong for 400 ms before a rep is abandoned. The regression test lies flat for three seconds mid-set and still counts six of six.
A promise that was false. Before writing the first-launch note — your video and progress never leave this device — I read the vendored bundle. MediaPipe posts usage statistics to Google every 60 seconds, with no switch to turn it off. The logger is now patched off, the CSP would refuse it anyway, and the note was written only after a network log showed it to be true.
The lesson that stuck: a false positive has a cause, and a threshold cannot name it. Out of frame, squatting and sitting still all crossed the same number. Fixing them meant asking what was physically different — and then proving each answer against footage of real people.
There is a great deal more
The full study covers the rest: the honest trade-offs table, all seven decisions with the options that lost, the accuracy numbers before and after each fix and how to rerun them, every bug and what found it, and what is deliberately not built.