Inspiration

I've spent years in the gym and years cycling through workout trackers, and every one of them was missing something I actually needed: the set types I use, supersets that behave, training volume that's counted correctly for dumbbells and single-arm work, a rest timer that stays out of the way. And most of them wanted an account, a subscription, and my data on their server before I'd logged a single set.

So I built GainBook: a strength-training tracker for people who take lifting seriously. It's fast, detailed, fully offline, and everything stays on your device. I train with it every week, and whatever I find missing gets built.

What it does

  • Plan: workout templates with target reps, weight, rest and set types (warm-up, working, drop set, failure), supersets with synced sets, and an onboarding flow that generates a starting plan from a few questions.
  • Track: quick set logging, automatic rest timer with sounds and notifications, replace or skip exercises mid-workout, timed sets, a live workout bar, and a Live Activity on the Dynamic Island.
  • Apple Health: live heart rate from Apple Watch or AirPods, per-set max heart rate and energy, a low heart-rate alarm for zone work, and workouts plus perceived effort written back to Health.
  • Review: history with a calendar, a full per-set breakdown, progress analytics for key lifts, and a muscle heatmap that shows where your training volume actually went.
  • Own your data: no account, no ads, no internet needed. Export the whole database as a file, back up to iCloud Drive automatically, and Android Auto Backup restores it on reinstall.
  • English, Spanish and Russian, light and dark themes, kg and lb, and accent colors that also change the home-screen icon.

How I built it

GainBook is Expo SDK 57 / React Native 0.86 on the New Architecture, with Expo Router for navigation. All data lives in SQLite via Drizzle ORM. Animations and drag-to-reorder use Reanimated and Gesture Handler, the body map is react-native-svg, and lists are FlashList. There are custom Expo native modules in Swift for iCloud Drive backups and the Live Activity, plus config plugins for Android Auto Backup rules, iOS signing and share-sheet import. Sentry handles crash reporting, PostHog handles product analytics, and EAS handles builds, submissions and OTA updates.

Tests: Jest unit tests cover the math and the migrations, and Maestro end-to-end flows run on both simulators against fake HealthKit and iCloud transports.

The muscle volume model

This is the part I'm proudest of. A set's volume is

$$ V = (w_{\text{body}} \cdot p_{\text{bw}} + w_{\text{logged}}) \times \text{reps} \times m $$

where \( p_{\text{bw}} \) is the share of body weight the movement actually moves and \( m \) is the per-hand or per-leg multiplier. That total is then divided across every muscle worked, using a distribution that sums to exactly 100%. So a bent-over row credits about 42% to middle back and 32% to lats, instead of "back: primary, lats: secondary".

I reviewed all 892 exercises in the library one by one, judging each on its own mechanics. Body-weight load comes from biomechanics segment-mass tables, not from guessing by exercise name.

The heatmap also normalises by strength capacity, because muscles aren't comparable at equal tonnage: a 98 kg bench press is roughly the same effort as a 230 kg leg press. Each muscle's share is divided by what that group can realistically handle, so the map shows where the work went, not just which muscle moves the most weight.

Challenges

  • OTA updates that silently reached no one. With fingerprint-based runtime versions, an eslint-only commit changed the hash (npm scripts were being fingerprinted) and stranded every installed binary. Nothing errored; the update was just never offered. I tracked down every non-native input to the fingerprint and excluded it, and now I check with eas fingerprint:compare before publishing.
  • A migration squash that crash-looped existing installs. Before the first release I squashed the migration chain. Regenerating it re-stamped the init migration with a future timestamp, so Drizzle re-ran CREATE TABLE on devices that already had the tables. Fresh installs were fine; upgrades crashed on launch. The fix was restoring the original timestamp, and a test now models Drizzle's replay rule against real SQLite.
  • Backups people can actually trust. One file format everywhere: a share-sheet export, an iCloud backup and a drag-out from the Files app are byte-identical. Exports are now zipped (~8× smaller), but import sniffs the file's leading bytes and will always accept the old bare .db files already sitting in people's iCloud folders.
  • A heatmap that told you nothing. The first version compared every muscle to a "maximal session" reference. A full-body workout spreads volume across ten muscles, so nothing ever lit up, and every workout looked like the same faint haze. Changing it to resolve the whole map together fixed that.

What I learned

Local-first is a real product advantage, but it moves all the responsibility onto the device. Migrations, backups and update compatibility have no server to hide behind, and their failures are silent. The most useful thing I did was write each lesson down as a test or a documented rule the moment it happened.

What's next

GainBook Pro: an optional paid tier (AI coach, plan import, cosmetic themes) on top of a free app that stays complete. The entitlement layer is already in the code, and it's built to plug into RevenueCat for StoreKit and Google Play Billing, with the core tracker staying free and offline.

Built With

Share this project:

Updates

Submission history