One call before every run
An iOS running coach that reads your recovery each morning and makes one call: run it, ease in, or back off. Designed and built solo in SwiftUI.
One call before every run
Every run starts with the same questions. Am I recovered enough to do what the plan says? What do I wear? What cadence do I hold? Strava can't answer them, because it measures runs after they happen. Cado is an iOS app I designed and built to answer them before.
Each morning it reads your HRV, resting heart rate, and sleep against your own baselines, checks the plan, and makes one call: run it as written, ease in, or back off. Then it tells you why. All of it is real and native: SwiftUI, SwiftData, HealthKit, Strava OAuth, and a live Claude-powered coach.
What success looks like
A solo project needs targets the same way client work does, because without them every call becomes a taste call. I set three before writing code:
From Runko to cado
Cado started as a React Native prototype called Runko: gold background, wobbly buttons, a hand-drawn running buddy. Testing split it cleanly. The character earned warmth, but the layout under it failed at the job of telling you what today's run should be. The app couldn't decide if it was a companion or a tool.
I rebuilt it native in SwiftUI and repositioned it as a data-driven coach. The daily check-in became the core loop: recovery in, one decision out. The hand-drawn layer survived as accents, because personality works as a layer, not a structure. Lettering, icons, and exactly one sketch-border focus object per tab.
I also kept it light-mode only. Ink on paper is the identity; the dark premium-dashboard look is what every competitor already ships.
The race bib
Clean and data-forward turned out to be where every fitness app converges, and cado was starting to look generic. So I committed to an identity: the race bib. The brand red stops being an accent and becomes the field. Cream text and paper cards sit on it, black ink buttons anchor actions, and every token is contrast-checked against the red.
The discipline that makes it work: red means action, nothing else. At one point the cadence number and the Open Train button competed in the same red, so the number went to ink and the run name became the page's only display-font moment. One screen, one loud thing.
Quiet when it agrees, loud when it dissents
The readiness pill was the app's reason to exist, and it was placebo. It said "Run as planned · 57%" nearly every day because the math ran against a hardcoded HRV baseline, and the percentage implied precision that didn't exist. A signal that never varies is furniture.
I inverted it. Cado builds your norms from 30 days of HealthKit history and stays quiet when things look fine: one line, "All clear." When recovery genuinely dips it gets loud: a tinted block with the verdict, the actual numbers as evidence, and a session-matched alternative. The derived score died everywhere in the app, because a number you can audit earns trust and a tidy 87% can't.
Run to a beat
Cadence is the one thing cado does that trackers don't: a metronome you run to, with per-run targets and a hand-lettered BPM you can read mid-stride.
The suggested cadence started as a hardcoded table by run type. That bothered me because Strava already reports each runner's real cadence, and the app was throwing it away. Now cado takes the median of your recent runs, corrects Strava's single-leg counting, and re-anchors the whole table to you. It waits for three runs of data before it trusts itself, shows a range instead of a single number, and says where the number came from. Cadence isn't a value to nail; it's a beat to settle into.
A plan you can scan
The plan library holds 89 real schedules from Pfitzinger, Hansons, and Higdon. A flat list of 89 is a wall, so browsing became distance first, then plan family, with the mile and kilometer twins merged. You're choosing, not scrolling.
The week itself is a paper card on the red field: a seven-day intensity bar, rows you check off, and jiggle-to-swap for moving workouts in place. It replaced a drawer flow that took three taps to do what a drag does in one.
A coach that pushes back once
The coach runs on Claude with your live recovery and 28 days of Strava load as context. The voice took as much design as the screens: lead with your real numbers, explain the why, push back once if you're overriding a bad day, then defer. You're the athlete.
The morning brief is capped at 50 words because it's read standing in a doorway. Model output gets sanitized at the UI boundary, since raw markdown asterisks read as an API response instead of a coach. And without an API key, the offline stub argues from the same evidence under the same rules, because the voice is the product, not the model behind it.
Real data, honest errors
Nothing in cado is mocked. Strava connects over OAuth, HealthKit supplies recovery, and an Anthropic key in the Keychain turns on live coaching. The Me tab is where the three sources light up.
Wiring real services taught the honesty lessons. An OAuth redirect-host mismatch and API failures that silently presented as "offline" both got the same fix: surface the real error, never fake a state. The same posture says "still learning your baseline" when there isn't enough data to judge. A full accessibility audit caught body text ignoring Dynamic Type at 124 call sites; the fix was one file.
What I learned
Three things I'd carry into any product:
Personality is a layer, not a structure. Hand-drawn accents over a data-forward layout survived every iteration. Hand-drawn everything did not.
Evidence beats scores. A readiness call you can audit earns more trust than a tidy percentage. Show the number and its source.
Recommendations belong at the decision point. Targets moved onto Today, the shoe call into the outfit grid, each reason generated at the branch that picked it. Advice that lives two taps away doesn't exist.
Home
Cado