Start a project
Case study / 03

WODFuel

Pick the format, add the movements, get the calories. A free app funded by ads, with a Pro tier on RevenueCat. No backend, no account.

  • Own product
  • 2025–2026
  • iOS & Android
  • Role: everything
  • Status: live on Google Play
WODFuel: logging an AMRAP with its movements
01
WODFuel: the calorie result with the breakdown by movement
02
WODFuel: history with weekly stats and streak
03
01 — ContextProduct

A CrossFit class ends with a score: rounds, reps, a time. It never ends with a number of calories, and fitness trackers guess badly at it because a WOD mixes barbells, gymnastics and machines in twenty minutes. WODFuel exists to put a defensible number on that session.

The first version shipped in 2025 as a proof of the idea: one screen, four workout formats, a calorie estimate and a banner. Version 2.0 is the product: a rebrand, a real history, a Pro tier and the observability needed to run it as a business rather than a side project.

02 — ProblemProduct

Estimating calories for interval training is not a lookup. An AMRAP scores reps per round times rounds done; an EMOM repeats each block every minute; a Tabata is twenty seconds on and ten off. Each format needs its own arithmetic before any physiology applies, and the user should never have to do it.

On the business side, a fitness utility has to be free to get installs, but free with no revenue is a hobby. The app needed a model where the free tier is genuinely useful, the paid tier is clearly better, and neither requires a server or an account to run.

03 — SolutionProduct

A React Native app with one loop: choose the format, enter the score, add the movements, tap "Calculate burn". The result is one big number, the summary of the format and, for Pro users, which movements burned the most. Every workout can be saved, revisited and repeated from the history.

The estimate runs on the device. A table of MET values and seconds per rep for 58 movements feeds a pure TypeScript engine; the format rules expand the reps and derive the total duration; the higher of the per-movement and the whole-session estimate wins. Free users see a banner and an interstitial after each calculation; WODFuel Pro removes the ads and unlocks unlimited history, weekly stats, streaks and the breakdown.

04 — Role

Everything. Product, the calorie model, brand and icons, the React Native app, the ad integration, the RevenueCat paywall and entitlements, EAS builds and over-the-air updates, and the observability set-up. Version 2.0 also meant moving the codebase to Expo SDK 57 and Bun without losing the profiles of existing users.

05 — ArchitectureEngineering
CALORIE ENGINEMET table · pure TSAPPExpo / React NativeHISTORY · PROFILEAsyncStorage, on deviceREVENUECAT · PROADMOB · FREE TIERAPP STORE · PLAYNO BACKEND · NO ACCOUNTEAS OBSERVE · INSIGHTSEAS UPDATE · OTA

Key decisions, and why:

  • No backend, again. Profile and history live in AsyncStorage. Calories are deterministic given the inputs, so there is nothing to sync and nothing to pay for per user. Purchases are the one thing that needs a server, and RevenueCat is that server.
  • The engine is a library, the app is a client. The calorie module has no React in it: types, a MET table and one estimate function. The workout form model, validation and format rules sit in a second pure module. Screens only wire state to components, which keeps the maths testable and the UI replaceable.
  • Two tiers, one entitlement. Every gate in the app asks a single boolean from one context. Ads never mount when it is true, the paywall is one function call away from anywhere, and a flag can force Pro for screenshots. Limits are constants, not conditions scattered through screens.
  • Measured before it is public. EAS Observe wraps the root and every screen marks itself interactive, EAS Insights counts launches and EAS Update ships fixes without a store review. A one-person product cannot afford to learn about a crash from a review.
06 — Technology
MobileReact Native 0.86 · Expo SDK 57 · Expo Router · TypeScript · React Compiler
EngineMET table, 58 movements · Pure TypeScript · AsyncStorage
RevenueRevenueCat · Customer Center · Google AdMob
OpsEAS Build · EAS Update · EAS Observe · EAS Insights
Toolingbun · ESLint · resvg icon pipeline
07 — ChallengesEngineering
  • AMRAP maths without a spreadsheet. Reps are entered per round, but the score is rounds plus a partial round. The engine multiplies each movement by the rounds completed and spreads the extra reps across movements in proportion to their share of a round, so "4 rounds + 15 reps" gives every exercise a fair count.
  • Ads that never touch a paying user. The banner waits for the entitlement check before rendering and collapses until an ad has loaded, so there is no grey hole and no flash of ad for Pro. The interstitial is created only for free users, preloaded, capped to one every three minutes and awaited until it closes before the result appears.
  • Four SDK versions in one jump. Going from SDK 53 to 57 meant a new Reanimated, a new React, lint rules that forbid setting state inside effects and a stale native project. Regenerating the native folders and treating every lint error as real was faster than patching around them.
08 — Result

A finished 2.0: a rebranded app with a persistent history, a working free/Pro split, ads that respect the entitlement and a dashboard that shows startup times, routes and errors from the first build. The work left is on the store side: the RevenueCat offering and the iOS ad units.

For ZubiLabs it fills the gap between the other two case studies: a product with a revenue model that is neither a one-time purchase nor a platform with a backend.

09 — Lessons
  • Save the thing the user made. Version 1 calculated and forgot. Persisting the workout turned a calculator into a product and made a Pro tier possible.
  • Monetise the friction, not the feature. Ads on the result and a small history cap are enough reason to upgrade; hiding the calorie number would have made the free app useless.
  • Rebrand and re-platform together. Doing the SDK upgrade in the same pass as the redesign meant every file was touched once, with the new conventions, instead of twice.