Case Study · 2026
DailyMove
A home movement app built around a constraint most fitness apps ignore: the person using it does not want to be disciplined.
Behavioral product design · Constraint-driven scope · Offline architecture
The brief
Build something that makes a body more capable, without ever making a person feel guilty.
Most training apps optimise for a number on a scale and an unbroken row of days on a calendar. This one was for someone who wanted neither — and who would quietly stop opening anything that turned a missed week into a failure.
The goal was stated as a capability rather than a shape:
Legs that can stand you up. Hips that can carry the shopping. A core that keeps you steady. Lungs with room to spare. For the next thirty years — not the next twelve weeks.
Which turns into a short, unusually strict specification:
- One room, one mat. No door anchors, no pull-up bar, no gym.
- Equipment is fixed: one kettlebell, one resistance band, one mat.
- Back-safe by default — hip hinge taught before load, no loaded spinal flexion.
- Assume very little starting upper-body strength. The first sessions have to be winnable.
- No dietary rules. No weigh-ins. No streaks. No body-shape metrics of any kind.
Decisions
Most of the work was subtraction — which is exactly the part a running app cannot show you.
The streak counter
The first build had a flame and N day streak in the top-right corner. That is the default shape for this category; it arrives without anyone deciding on it.
It is also a guilt mechanism. The entire psychological force of a streak comes from it hurting to break — which is the precise opposite of what this product promises. A week off is fine. You do not owe it back.
Replaced with a flat statement of fact: 12 days active, last 30. It moves down when you move less, and it never accuses you of anything.
The complete button is never disabled
Originally you had to tick all eleven exercises before Complete would light up. So: do seven, decide that is enough, and the app informs you that you did not finish.
That contradicts the only instruction the programme actually gives — stop when it is enough. A rule the interface refuses to honour is not a rule.
The button is now always live and its label follows you: I moved today at zero, Log these 4 at four, Did all of it at eleven.
The log records what you actually did
Easy to miss, and worse than the button: the original saved the whole session template regardless of what you ticked. Seven exercises done, eleven recorded.
The history looked better than the truth. A log that flatters you is not a log — it is decoration. It now stores only what was checked, alongside what was offered.
The metric is how the body felt
After a session the app asks exactly one question: easy / just right / a bit hard.
Over months that produces the thing worth having — the same session drifting from a bit hard toward just right. It is a personal record no scale can report, and it is the only progress chart in the app.
A learning path that is not training
The app quietly assumed you already knew how to perform eleven movements. The person it was built for had never done any of them.
So: six movements, ordered by risk and foundation rather than by training order, one at a time, each with a physiotherapist video and the two or three cues that actually matter. Marking one learned means you know roughly how it goes — explicitly not that you are good at it.
Under the hood
Two problems where the obvious implementation was quietly wrong.
Every evening session was filed under tomorrow
Dates came from toISOString(), which returns UTC. On the West Coast that means
any session finished after 5pm was recorded as the following day. The calendar, the
counters, the already logged today check — all wrong, and all silent.
Dates are now derived from local components. Existing records were repaired rather than discarded: each entry already carried a correct timestamp, so the true local date could be recomputed from it during migration.
A cache strategy that did not depend on remembering
The app works offline, and the first service worker served everything cache-first.
But its filenames carry no content hash — app.js is always app.js —
so the browser has no way to know it changed. Shipping an update required manually
bumping a version constant. A mechanism that depends on a human remembering is a
mechanism that will fail.
Now HTML, CSS and JavaScript are network-first with a three-second fallback to cache; images and fonts stay cache-first. JavaScript has to be network-first too, because the script and the markup are coupled — the element IDs the script looks up are written in the HTML, so a new page paired with an old script renders nothing at all.
Production systems solve this with hashed filenames and atomic updates. Without a build step, the honest alternative is to fetch the coupled files together and accept the few milliseconds.
One source of truth
The exercise library existed twice — once as hand-written markup, once as a JavaScript array. They had already drifted: one exercise's identifier did not match between the two, so its jump to details link silently never rendered at all.
Thirteen exercises now live in one array and the markup is generated from it. 489 lines of duplicated HTML went away, and that entire class of bug went with them.
How this was built
I defined the product brief, behavioral rules, edge cases, and acceptance criteria, and directed the work across several development sessions. An AI coding agent produced the implementation; I reviewed the resulting behavior, tested edge cases, and drove revisions. The source is private.
The brief said a missed week is fine. The first build still shipped a streak counter — the default shape of the category arrives whether or not anyone decides on it. Reconciling those two things was most of the work.
I caught Chinese text left in the English interface from a screenshot, which became a one-directional guard in the tests.
The guard is one-directional rather than symmetric because the Chinese interface is a workshop I use daily, while the English one is a shop window a stranger sees once. The two do not deserve the same standard — and that same distinction is what sent the demo to English by default.
Where it is now
In daily use. Static site, no backend, no accounts — the log lives in the browser. Which is also why the public link can simply be the real app: every visitor gets their own empty copy, and there is nothing of mine on a server to separate out.
The demo link seeds three weeks of fictional history so the page has something to show. It is stored under its own namespace and never touches a real log.