I was coming back from a shoulder thing, which meant working out on a plan with phases — what’s allowed now, what’s allowed next month, what’s off the table entirely. Every fitness app I looked at wanted a subscription, an account, my data in their cloud, and none of them understood “phase 1 means this specific list of movements and nothing spicy.” What I actually needed was small: show me today’s workout, respect the current phase, let me log it in one tap.

So that became fit. — built and deployed in a day, living on Randy with the rest of the family apps.

Phases with unit tests

The design is almost aggressively simple. The phase logic is keyed to date ranges: the app knows what phase the calendar says I’m in, and every workout it produces stays inside that phase’s rules — including caps on how hard anything involving the shoulder is allowed to push. That logic has actual unit tests, because it’s the one part where a bug has consequences I’d feel in the morning.

Lazy, cached generation

The daily workout itself is generated by a small, cheap AI model — given the phase, the constraints, and the training split, it writes the day’s session. Generation is lazy and cached: the first request of the day generates and saves the workout, everyone after that (which is also me; it’s a single-user app) gets the cached copy. No scheduled jobs, no API spend on days I don’t open it.

One tap, one JSON file

Logging is the part I optimized hardest, because friction kills habit trackers. There’s a one-click “done” endpoint: it grabs today’s cached workout and writes the whole thing — date, phase, focus, exercise list — into the log as a completed entry. Finishing a workout is one tap. The log is a JSON file on disk with an ID and a timestamp per entry — the entire database. It gets backed up with everything else on Randy, and I can read my own training history with cat.

No doorman

There’s no login screen. The app is reachable only inside my private network, and I’ve stopped building doormen for apps that live behind a locked door — anyone who can reach it is already in my house. (This philosophy got its own post later, after a different app’s login screen betrayed me.)

An afternoon and a favicon

The stack is the same one I use for everything, which is most of why it took a day and not a week: a web framework I already know, a container on Randy, the reverse proxy that fronts the other family apps, one new hostname. No new services, no new bills, no new accounts. The marginal cost of a new self-hosted app, once the rack exists, is an afternoon and a favicon.

The shoulder’s doing fine, thanks for asking. The app outlived the recovery plan, got new phases, and eventually developed a much more interesting failure mode: it started generating workouts nobody was actually doing.