-
-
Privacy per post — private activity still earns XP and holds the streak
-
The profile: a level ring around your avatar, streak, XP to the next level, and everything you logged
-
Three goal kinds that match how people actually train: streak, frequency, volume
-
Badge unlocks are shareable moments, drawn around the mascot
-
Global leaderboards: rank by XP, streak or level — filter to your city, sport or friends
-
Every session becomes a story-ready share card with a scannable code
-
The badge wall — original Fouzi artwork across four rarity tiers
-
A feed that only rewards doing the work — no influencers, no ads
-
App icon — 1024×1024, uncropped
Inspiration
Nobody quits the gym because they're lazy. They quit because nothing notices. No streak breaks, nobody asks where you were on Tuesday.
I watched the same pattern everywhere I've lived and trained: people who train alone quit quietly, and people with a padel group that expects them on Thursday keep showing up for years. The difference isn't willpower — it's that someone (or something) notices.
Fitness trackers record what you did. Duolingo made a language stick by making absence visible. Nobody had done that properly for sport — across every sport, not just running with a GPS watch. So I built Fouz: a tracker wearing a game's clothes, where showing up is the game.
What it does
Log any of 55 sports in about 20 seconds — padel, football, gym, boxing, yoga, no wearable needed. Then the game layer takes over:
- XP with public math: a workout pays 20 XP, a pickup game 15, a meal 10, a goal 10. Level N costs 100 × (N−1)² XP. No hidden multipliers, no pay-to-win — the formula is right there in the app.
- Streaks that stay honest: alive only while your last logged day is today or yesterday. No freeze tokens, no streak insurance. That's what makes a 30-day streak mean something.
- Badges you'd actually screenshot: original artwork of Fouzi (the mascot) for every milestone, across four rarity tiers.
- A crew: a feed that only rewards doing the work (no influencers, no ads), leaderboards filterable by city, sport, or friends, and pickup games near you — see games starting within two hours, join in one tap, or host one and let the neighbourhood fill it.
- Share cards: every session becomes a story-ready card with your level and a scannable code that lands friends in the app.
- Privacy per post: public, followers, or just you — and private activity still earns XP and holds your streak.
Fouz launched global from day one: English, French, and Spanish, with leaderboards live from Casablanca to Kuala Lumpur.
How I built it
Fouz is built by one person — design, code, backend, artwork direction, and the marketing site.
- App: React Native + Expo (TypeScript), EAS builds.
- Backend: Supabase — Postgres with row-level security, RPCs for everything the client shouldn't compute, storage for share-card images.
- Monetization: RevenueCat (
react-native-purchases) powering Fouz Pro and the Founder's Pass — more below, because the integration details matter. - Web: A Next.js site (fouz.app) where every in-app share becomes a public permalink with a proper OG unfurl, plus a localized marketing/SEO surface, with the install funnel instrumented in PostHog behind a first-party proxy so ad blockers don't blind it.
How RevenueCat is wired in (the part I'm proud of)
Fouz is a game with public leaderboards, so monetization had one hard constraint before it had a price: nothing you can buy may touch XP, levels, streaks, badges, or leaderboard rank. The moment money buys rank, every rank is meaningless and the free players — the community that makes the leaderboard worth topping — leave. The paywall says it out loud, directly under the price: XP and rank stay earned, never bought.
So Pro sells leverage over your own data and reach, never over another athlete: Pro search filters and leaderboards, your season in XP / volume / records, unlimited log templates, who viewed your profile, and every share-card style. Three SKUs across two RevenueCat offerings — Pro Monthly $3.99, Pro Yearly $24.99 with a 7-day free trial (the default; the button reads "Start your free week"), and the Founder's Pass at $49.99 one-time — Fouz Pro for life, plus a Founder mark.
The integration is deliberately paranoid:
- Purchases bind to the Supabase user id via
Purchases.logIn, so an entitlement survives reinstalls and device changes. - The webhook is the authority, not the client. A
revenuecat-webhookedge function writesprofiles.founder_atand the Pro expiry server-side, so the app can never grant itself anything. Its decision layer is a pure, unit-tested module:NON_RENEWING_PURCHASE/INITIAL_PURCHASE/TRANSFERgrant,REFUND/EXPIRATIONrevoke, and every unrecognised event type is ignored — RevenueCat adds event types over time, and an unknown one must never read as a grant.CANCELLATIONdeliberately does not revoke Pro: auto-renew switched off still means paid until the term ends. - Billing fails closed. One module owns the SDK and no call throws into the UI. On Android, on a build with no RevenueCat key, or on a JS bundle that reached a binary predating the native module (
react-native-purchasescannot ship over an Expo OTA update), the paywall simply does not render — a purchase button that cannot complete a purchase is worse than no button. - Offerings are swappable from the dashboard. Both the
proandfounderofferings read RevenueCat'scurrentfirst, so pricing can move, or the Pass can be withdrawn, without shipping an app update — which is exactly how a founder offer should end.
Challenges I ran into
- Sharing had to survive real names. Share-page unfurls are composited from the exact PNG the app rendered, because server-side text rendering (Satori) crashes on Arabic letter-joining — and my launch cities include Casablanca and Cairo. Compositing the app's own image sidesteps the text path entirely and means the unfurl matches the card pixel-for-pixel.
- OTA updates vs. native modules.
react-native-purchasescan't ship over an Expo OTA update, so a JS bundle can absolutely land on a binary that predates the native module. The billing layer detects that state and hides itself instead of crashing. - Streak honesty is a design constraint, not a feature. Every "kind" mechanic (freeze tokens, grace days) makes the streak worth less to the person holding it. Saying no repeatedly was harder than building it.
- Solo scope discipline: every feature had to earn its place against "does this get someone to log tomorrow?"
Accomplishments I'm proud of
- Fouz is live on the App Store — submitted, reviewed and released on 28 August 2026, inside the Shipaton window.
- The entire loop ships: log → XP → level → share → friends react → someone needs a fourth on Thursday → you show up again.
- 55 sports, 9 categories, three languages, day-one global leaderboards.
- Original badge and mascot artwork across four rarity tiers — not an asset pack.
- A monetization model I can defend to every free user in the app, in one sentence, without flinching.
What I learned
- Consistency is a social problem wearing a fitness costume. The XP is the hook; the crew is the retention.
- Fail-closed beats feature-flagged: the billing layer that refuses to render is the one that never crashes a paying customer.
- Shipping alone means the product is your prioritization function, and "no" is the most-used feature.
What's next for Fouz
- It shipped. Fouz went live on the App Store on 28 August 2026 — listing, review and release all inside Shipaton. The final boss turned out to be App Review, twice.
- Getting the first athletes to log a second day: the only number that matters before any other number matters.
- Android next, then richer pickup-game tooling (recurring games, skill matching).
- RevenueCat paywall experiments on how Pro and the Pass are presented. The no-pay-to-win rule is non-negotiable; how it gets told is the experiment.
- More badge artwork, more share-card styles, and the boring, compounding work of shipping what the early athletes actually ask for.
Built With
- expo.io
- ios
- next.js
- postgresql
- posthog
- react
- react-native
- revenuecat
- supabase
- typescript