For a while now I have been building a running app called Smart Runner, and the whole thing is a bet against how the category usually works. No subscription. No account. No server. Everything lives on the phone. Here is why, and the parts that were actually hard.
The bet
Most serious running apps are subscriptions, and your training history sits on their servers. If you stop paying, you lose access to years of your own runs. If the company gets acquired or shuts a feature, your data goes with it.
I wanted the opposite. Pay once, and the data never leaves your device. The app reads from Apple Health and writes to local storage. There is no Smart Runner backend to leak, sell, or sunset.
The stack
- SwiftUI + SwiftData for everything local. No backend at all.
- HealthKit for reading workouts, heart rate samples, and writing structured workouts back.
- WatchConnectivity to push the planned workout to the Apple Watch and pull the result back.
- A native watchOS app that plays the workout back with live pace and HR zones.
The part that bit me
WatchConnectivity plus SwiftData is where I lost the most time. I was handing SwiftData model objects to the off-thread send, and the model context would get touched off its own thread and corrupt state. Intermittent, ugly, hard to reproduce.
The fix was to snapshot the planned workout into plain value types (a dictionary of [String: Any]) on the main context before the background send, so nothing model-bound ever crosses a thread boundary:
// Wrong: passing the SwiftData model into the off-thread send
session.transferUserInfo(encode(plannedWorkout))
// Right: snapshot to value types on-context first, then send
let snapshot = plannedWorkout.asTransferDictionary() // [String: Any], no model refs
DispatchQueue.global().async {
session.transferUserInfo(snapshot)
}
Obvious in hindsight. Most concurrency bugs are.
The training engine
The coaching side is built on published running science rather than a black box:
- VDOT for pace zones, from Daniels and Gilbert's 1979 oxygen-cost model. One race result gives you Easy, Marathon, Threshold, Interval, and Repetition paces.
- ATL / CTL / TSB training-load tracking, the same acute-versus-chronic load model endurance coaches use, so the plan knows when you are fresh, fit, or cooked.
- The plan recalculates after every run instead of being a fixed 16 week PDF.
If you want the math, there is a free VDOT calculator that exposes the numbers.
Why on-device at all
Three reasons that turned out to matter more than I expected:
- Your training history should outlive any subscription. Pay once, keep it forever.
- There is no account step, so onboarding is just opening the app.
- No server means no server bills, which is part of how the pay-once model even works.
The trade-off is real: no web dashboard, no cross-platform sync beyond Apple Health, iPhone and Apple Watch only. For the runner who wants a private, owned training tool, that trade is the point.
If you want to see it, it is here: Smart Runner. Happy to get into the watch sync or the training-load math in the comments.
Top comments (0)