01Context
I had tried six habit trackers and uninstalled all of them within nine days. The pattern was always the same: I'd set up the app on a Sunday, feel briefly virtuous, miss a day, see a giant red broken-streak icon, feel slightly worse than before I installed the app, and uninstall it.
The problem wasn't the tracking. The problem was the tone. These apps were yelling at me like a 4am drill sergeant for forgetting to floss. I wanted something that nudged me like a friend who knew when to shut up.
02The problem
Three things, in order of how much they bothered me:
- Streak anxiety. The streak is a hostage situation. Lose one day, lose the streak, lose the user. Apps treat this as a feature.
- Bad timing on reminders. Reminders fire at the same time every day, regardless of what you're doing. This is how an app becomes an enemy.
- Cloud bloat. No tracker I tried respected the fact that habit data is, at most, a few kilobytes per month. Most demanded an account, a sync, and a paid tier.
"If your reminder app has to apologize, the reminder is the problem, not the user." - a note I wrote on the back of a receipt at month 1
03Approach
I built Loopy around three constraints I refused to break:
- Data lives on the device. Sync is opt-in via CloudKit. There is no server.
- Streaks are real, but they are quiet. They live on a detail screen. The home screen shows something more honest.
- The roast is on-device, model-generated, and tuned for tone. No telemetry leaves the phone, not even crash reports without consent.
04Design choices
The biggest design decision was what to put on the home screen
instead of a streak. After three weeks of trying versions, I
landed on a small textual phrase: "Showed up 5 of the
last 7 days." It's the truest sentence about a habit,
and it doesn't punish a single bad day.
For typography I went with system font at heavy weights, with one accent monospace for numbers. Everything is grayscale except a single calm green tag for "showed up today" - the color shows up exactly twice per day, max. Scarcity makes color mean something.
05Stack
One iOS app, one tiny Rust core for the parts that need to outlive the platform.
app/ SwiftUI 5 - all UI, animations
core/ Rust - habit model, scheduling, roast tuning
shared/ UniFFI - shared types, generated bindings
data/ SQLite - one file per device, no server
sync/ CloudKit - optional, e2e, opt-in The roast model is a 380 MB Llama-class distill, quantized and bundled. It runs in <200ms on an A17. It never sees the network. I am, on principle, allergic to features that require a server for a thing that fundamentally doesn't.
06Outcomes
Loopy shipped in March 2025. It is small on purpose. The numbers are also small on purpose. I count them anyway.
The reviews I keep are the ones that say "this is the first habit app I haven't deleted." The reviews I do not keep are the ones asking for a streak on the home screen. The streak is on the detail screen. It is fine where it is.
07Learnings
- Tone is a feature. It's the feature, on this product.
- Local-first is a UX choice as much as an engineering one. Users can feel the absence of a sign-in flow without being able to name it.
- An on-device model is a constraint that improves the product. A bigger model would have made the roasts worse - more generic, more polished, less specific. Smaller forced me to handwrite the system prompt with care.
- Build the second screen first. The home screen was the last thing I designed. Everything else informed it. If I'd designed it first, it would have a streak on it.