There Are a Million Fitness Apps. I Built My Own Anyway.
There’s no shortage of apps for tracking a workout or a weigh-in. Dozens of good ones, most of them cheap, some of them free. So the honest question isn’t “why not pay for one” — it’s why I built my own instead of just downloading one.
The answer isn’t the money. It’s that building a custom app got cheap enough that “exactly what I want” stopped being a luxury.
What I needed, and what got built
The need was ordinary. A weigh-in most mornings, with a trend I can trust instead of the daily noise. Workouts that follow a structure — blocks, sets, timed holds, exercises done per side — with a timer that runs the session so I don’t have to think mid-set. A weekly plan that tells me what today is, and a record of whether I did it. That’s it.
Every app I tried did two of those the way I wanted and the third its own way. And in all of them, the data was theirs.
So: Soma Tracker. A small web app that lives on my phone’s home screen. Weight logging with a trend line. A guided player that walks through blocks and timers, pauses, and resumes after you close the app. A weekly plan. Goals with live progress. History, by day and by month.



It was usable the same evening I started it — logging a weigh-in, running a workout. Everything since has been added the same way: I hit something while training, I describe it, it ships. It isn’t finished. It’s in daily use while it grows.
The stack
Personal project, my own time, my own choices — so here it is with nothing anonymized.
| Layer | What | Notes |
|---|---|---|
| Backend | ASP.NET Core minimal API on .NET 10 — one Program.cs | One endpoint per resource. No controllers, no DI ceremony I don’t need. |
| Database | SQLite in WAL mode, raw SQL via Microsoft.Data.Sqlite | No ORM. The schema is applied idempotently on startup. Safe to read from outside while the app is running. |
| Frontend | Vanilla-JS PWA: index.html, app.js, style.css, a service worker | No framework, no build step. Installs to the home screen, works offline. |
| Hosting | A systemd service on my own VPS, behind Caddy | Reachable only over my tailnet. No public exposure, so no auth layer to build — the network is the perimeter. |
| Development | Claude Code, from the terminal and from my phone | I describe the feature, the agent writes it against the codebase, I test it on the phone. |
A few notes on why it looks like this:
- Boring on purpose. Every choice shortens the distance between “I want X” and “X is on my phone.” No build step means the loop is edit → reload. No ORM means the agent and I read the same SQL.
- The stack didn’t get simpler. The cost of using it dropped. None of this is exotic; a .NET developer could have built it years ago. What’s different is that going from “I want a trend that isn’t fooled by a week without weigh-ins” to a working implementation is now a conversation, not an afternoon of typing.
- The lesson the PWA taught me. For weeks, changes took hours to show up on my phone even after closing the app. The culprit wasn’t the service worker — it was the browser’s HTTP cache: static files served without
Cache-Controlget a heuristic freshness, and a “network first”fetch()in the worker still goes through that cache. The fix is three layers deep:no-cacheon code, a version stamp in the asset URLs, andcache: 'no-store'in the worker. Now the version served is printed in the header, so “am I seeing the latest?” is a glance.
What custom actually buys you
Not the features. Any decent app has features. What you get is the two things a product can’t sell you.
Features on demand, on my schedule. A feature idea mid-workout is a sentence to Claude Code that evening. No backlog vote, no waiting for a product team to agree with me. The per-side exercises, the resumable session, the “I didn’t train today, on purpose” marker — each exists because I wanted it that week, and most of them landed the same day I asked for it.
My assistant reads and writes it directly. Aura is the app I built to talk to Claude Code sessions from my phone through a chat, with characterized assistants on top — each with its own persona and skills. One of them is Soma, the wellness one. Because the tracker is an API on localhost plus a SQLite file on the same machine, Soma doesn’t need an integration. I tell it “ran 5k this morning” in the chat and it posts the session to the API. I ask how the month is going and it queries the database. It has an evening check-in that spots gaps in the log and asks. And when I report a weight, it renders the trend chart from the app and drops it into the conversation.
None of that is a plugin, a webhook, or a product feature. It’s what you get for free when the data is a file on your own machine and the API is a port on localhost.
The data outlives the app. Months of sessions and weigh-ins in one SQLite file I can copy, query, or hand to the next tool. When I later built the app I use to run my own content pipeline, it borrowed this exact shape — minimal API, SQLite, vanilla PWA — because it had already proven itself here. Owning the format, not just the data, is what makes a set of small tools composable instead of a pile of apps that don’t talk to each other.
What I gave up
No app-store polish. No community, no social feed, no one else’s UX researcher having already solved the problem for me. If something’s clunky, I only have myself to blame — and myself to fix it. It’s a single-user tool on a private network; it will never scale past me, and it doesn’t need to.
In exchange: it fits the way I actually train, not the way a product team assumed I would. Nothing to cancel. And the database is mine, in a format anything I build next can read straight away — including whatever I build after this one.