-
-
Home Screen
-
Collections screen, where you have overview and spell selection
-
Battle Screen, you have Ranked, Friends(code or qr code scan),3 level of bots. You earn thophies gain XP and climb leagues.
-
Train Screen where you allow camera and choose type of excercise which you would like to record and earn XP.
-
Screen shot from Push Up training of camera live counting my reps.
-
Streak Screen counts only days with a full set of ten reps — steps alone never hold the chain.
-
Progress Screen · Walk reads steps from Apple Health. The dashed line is where XP stops paying.
-
Progress Screen · Overview puts today against the step cap, the streak set and the XP soft cap.
-
Progress Screen · Push-ups tracks best set per day — the number that decides your streak.
-
Achievements Screen spreads 21 medals across walking, training and battling. No shortcut.
Inspiration
I have the same problem as a lot of people who grew up on monster-collecting games: I will grind for hours for a number that goes up on a screen, but I will not do ten push-ups. The reward loop that games nailed decades ago is completely missing from fitness apps, which mostly offer guilt — streak shaming, calorie counting, before/after photos.
So I inverted it. In Zywo the XP bar is real. The only way to fill it is to actually move.
What it does
You raise a creature. It levels up only from your real physical activity — there is no tapping, no idle grind, no way to fake it.
Two things feed it:
- Everyday movement read from Apple Health / Health Connect: steps, runs, cycling distance.
- Reps counted by the phone camera — push-ups, squats and sit-ups — using on-device pose detection. The phone watches your body, measures joint angles, and counts.
XP unlocks 50 levels, spells, and three evolution stages per creature across five elements with a rock-paper-scissors type cycle. At level 8 you unlock turn-based PvP — and this is the part I like most: there is no physical activity inside a battle. You spend, tactically, what you earned on the floor.
Design rules I refused to break:
- Level and XP never go down. Missing a week is not punished.
- Paying is never pay-to-win — it buys coverage and access, never bigger numbers.
- Camera frames never leave the device.
How we built it
App — React Native 0.86 (bare) on the New Architecture, TypeScript,
Hermes, Expo SDK 56 modules, Zustand + TanStack Query, Reanimated 4 with
react-native-worklets, MMKV.
Rep counting — react-native-vision-camera v5 with a Nitro frame
processor feeding MoveNet Lightning (int8) through
react-native-fast-tflite. Everything runs on-device. A rep is a state machine
over joint angles with visibility gates, minimum rep duration, and tempo
variance tracking as an anti-cheat signal.
Creatures — 45 GLB models rendered in 3D with Three.js / React Three Fiber
over expo-gl.
Backend — Supabase. 58 numbered, immutable migrations. Row Level Security
on every table, default deny. The client has no INSERT/UPDATE/DELETE
privilege on any gameplay table — every state mutation goes through
SECURITY DEFINER Postgres functions. Damage, XP, levels and trophies are
computed server-side, never on the phone. All balance numbers live in a
game_config table so the game can be tuned without shipping an app update.
Monetization — RevenueCat. This is the part I'm most careful about:
react-native-purchases handles the storefront, but the paid flag is never
written by the client. RevenueCat webhooks hit a Supabase Edge Function, which
calls a SECURITY DEFINER RPC that applies the event idempotently (unique
event id), resolves the user through app_user_id / original_app_user_id /
aliases, and writes the entitlements row. The app only ever reads it. Every
webhook event is also stored raw, so subscription state is fully auditable.
Ads — AdMob rewarded ads for an optional XP bonus, and the reward is granted only through Server-Side Verification: the client never says "I watched an ad, pay me". Google's signed SSV callback does, and the grant is serialized per user per day in Postgres.
Challenges we ran into
The pose model loaded in development and not in production. On Android the
TFLite library opened the model with URL(path).readBytes(), which only works
while Metro is serving files. In a release build the model is an Android
resource and AAPT2 had renamed its path to res/S7.tflite. Users got "the pose
model could not be loaded" on the Play Store while everything worked locally. I
found it by unzipping the APK and running aapt2 dump resources, then patched
the library to resolve the model by resource name instead of path.
Frame timestamps are not in the same unit on iOS and Android. VisionCamera
documents Frame.timestamp as a presentation timestamp, but the unit is not
part of the contract — seconds from CMTime on iOS, nano/microseconds on
Android. My rep counter compared durations against a 500 ms minimum and
received seconds, so a 1.5 s push-up read as 1.5 < 500 and every rep was
rejected as "too fast". Fixed with a frame clock that detects the scale from
two consecutive plausible deltas and then locks it for the session —
locking matters, because a 10-second pause expressed in seconds looks exactly
like a perfectly plausible 10 ms frame gap.
Below 5 fps the counter silently stopped counting. On weaker Android phones the int8 model on CPU drops to 3–4 fps, and the bottom of a fast push-up (80–150 ms) simply never lands in a frame. Widening the plausible-gap window fixed the clock; a per-exercise, platform-aware angle tolerance fixed the counting, with the trade-off documented in code rather than hidden.
Writing my own QR encoder and scanner. Friend duels exchange a code visually, and the off-the-shelf options didn't fit the constraints of this camera pipeline, so both the encoder and the scanner are hand-rolled.
App Review. Three rejections taught me things no guideline summary did: a button before a permission prompt may not say "Allow", a custom permission message may not offer a way to skip the system prompt, and on a subscription paywall the billed amount must be visually dominant over the monthly equivalent.
Accomplishments that we're proud of
- Rep counting that works on a mid-range Android phone at 4 fps, entirely offline, with no video ever recorded or uploaded.
- A backend where cheating requires compromising Postgres, not the phone.
- An entitlement pipeline that is idempotent, auditable and impossible for the client to forge.
- A fitness app that never shames you. Vitality only affects PvP; XP and level are permanent.
What we learned
That the gap between "works in development" and "works in a signed release build" is where the real engineering is. Both of the worst bugs in this project were invisible in every test and every debug run, and both were only findable by inspecting the shipped artifact itself.
What's next for Zywo
Letterboxing the camera frame instead of centre-cropping it, so squats measure the ankle reliably. More creatures and evolutions. Guilds. And a mode aimed at parents who want their kid's screen time to cost push-ups.
Built With
- admob
- android
- deno
- expo.io
- google-sign-in
- health-connect
- healthkit
- hermes
- mmkv
- movenet
- postgresql
- react-native
- react-query
- react-three-fiber
- reanimated
- revenuecat
- sign-in-with-apple
- supabase
- tensorflow-lite
- three.js
- typescript
- vision-camera
- zustand
Log in or sign up for Devpost to join the conversation.