← All work

liftbook

A lifting log for iPhone that works like a paper notebook: type a movement, type your sets, and it quietly tells you what you did last time.

Year
2025, in the App Store since 2026
Made with
React Native, TypeScript, AsyncStorage, react-native-svg
Links
App StoreLive siteSource on GitHub
An early build logging a pull day. The UI has moved on; the notebook hasn't.
Why

I got tired of bloated fitness apps. Most want an account, a profile and a tour before you can log one set. I wanted something about as fast as writing it down, and nothing else.

So the rule is "as simple as a notebook": black and white, a handwritten font, ruled lines, and as little as possible between you and the next set. It was also my first app to make it all the way into the App Store.

How it works

One page, one footer

A lift is a notebook page: a date, a title like "Push Day," and movements with sets of weight × reps. All the typing happens in one footer that rides on the keyboard and changes mode as you go. There's no save button; every submit writes through.

Title→Movement→Weight × reps

Suggestions from your own history

There's no exercise database. Titles predict your rotation: after Push, Pull, Legs, Push, it suggests Pull. Movements come from the last time you did that same day, and matching is by word prefix, so "inc pr" finds Incline Press.

Sets get no suggestions, just a line under the input: last time: 135x8, 140x6. That replaced something cleverer. The app used to find the tightest cluster of your working sets from last time, to filter out warmups and drop sets, and suggest weights around it. The plain note took its place when the charts landed.

Nothing leaves the phone

Every lift lives on the device as one JSON blob in AsyncStorage. No accounts, no server. liftStore.ts reads it once per launch, serves everything from memory, and writes back through a serialized queue so fast edits can't interleave.

That makes the phone the only copy, so the store is paranoid. A corrupt blob gets copied to a backup key before starting fresh, and if the backup or the read itself fails, the app runs in memory for that session and refuses to write.

Stack

React Native 0.80TypeScript, Fabric, Hermes
AsyncStorageOne JSON blob, no backend
react-native-svgHand-drawn charts and the ruled lines
react-native-keyboard-controllerKeeps the footer on the keyboard, plus a small patch to React Native
JestThe store and the suggestion engine
Screens
App Store screenshot: the list of lifts, one dated row per workout, under the headline 'Finally, an uncomplicated fitness app' App Store screenshot: a Push Day page with sets logged under Fly and Tricep Pushdown, and movement suggestions above the keyboard App Store screenshot: the charts screen plotting max, average and min weight for Rope Tricep Pushdown App Store screenshot: entering weight and reps for Rope Tricep Pushdown with a 'last time' note under the input App Store screenshot: an empty list with a hand-drawn arrow pointing to the plus button and the liftbook logo

The App Store screenshots. Yes, the font is the whole personality.

What it taught me
  1. 1

    The best suggestion was the last workout

    A clustering algorithm for predicting weights lost to one line of text.

  2. 2

    Sometimes the bug is in node_modules

    The keyboard flickered on every mode switch because Fabric skips a reloadInputViews call, so I patched it back in.

  3. 3

    One state value beats five flags

    Collapsing the editor's focus logic into one descriptor deleted a pile of timers and race conditions.

  4. 4

    Local-only means you are the backup

    With no server copy, the store would rather not write than overwrite something it can't read.