Inspiration

For years I've dealt with recurring TMJ issues and stress coming from running my own business and dealing with a large family with challenges. Part of managing it is slowing down: breathing exercises, a quiet mind, a few minutes at a time, a few times a day. And every timer I tried was built backwards for that. A timer's whole job is to interrupt you — you set five minutes, a buzzer goes off, you're done. But interruption is exactly wrong for breathing, planks, stretching, cold plunges, or focused work. The point of those is to keep going.

I didn't want a countdown. I wanted a floor: at least fifteen minutes, with permission to continue. So I built AtLeast — the timer I actually wanted, and as it turns out, one nobody else had built.

What it does

AtLeast inverts the timer. You set a minimum — at least X minutes — and it taps your wrist gently at an interval you choose. Each tap means keep going. When you pass your floor, the taps simply stop. Silence is the success state.

That's the entire interaction, and it's deliberately eyes-free: I close my eyes, put ear plugs in, set at least 15 minutes, and breathe. There is no countdown, no progress ring, no remaining-time display anywhere in the app — that's a hard design rule, not a missing feature. You feel the app; you never look at it. The Apple Watch Taptic Engine is the whole interface, with six selectable haptic strengths from gentle to max and a live preview so you can feel your choice before you begin. The timer runs on-device with no account and no network required.

The iPhone app can also run a session on its own. Each timer can also carry a Session Type that saves the finished session to Apple Health, as Mindful Minutes or as a workout (Yoga, Mind & Body, Flexibility, Cooldown, Core Training, or Traditional Strength). AtLeast only writes to Health; it never reads it.

How I built it

  • watchOS-first, iPhone as companion. The watch runs the session; the iPhone configures presets and mirrors a live session ambiently (same rule there — no numbers, just an ambient pulse).
  • WKExtendedRuntimeSession keeps the haptic rhythm alive with the screen off — deliberately not HKWorkoutSession, which would have been the easy-but-wrong shortcut.
  • Absolute timestamps everywhere. The timer never counts elapsed seconds; it stores start and target Dates, so backgrounding, restarts, and clock drift can't corrupt a session.
  • SwiftUI .sensoryFeedback() drives every tap declaratively.
  • Watch↔phone sync: I built my own library, SundialKit, to use WatchConnectivity's application context as revisioned, latest-wins state snapshots with a heartbeat, so a dropped delivery self-heals — built for reliability first, features second.
  • Accessibility and localization as launch features, not promises: VoiceOver labels and announcements, Voice Control input labels, Dynamic Type, Reduce Motion, and six languages (English, Spanish, German, French, Japanese, Arabic).
  • Modular Swift packages with the core logic platform-portable and unit-tested (it even compiles for WASM in CI).

How I used RevenueCat

The core timer is free forever — including background reliability, which I refuse to monetize. AtLeast Pro is "memory": the free app deliberately stores nothing, and Pro is what remembers — saved custom presets and session history with streaks. RevenueCat powers the whole loop: subscriptions ($0.99/month, or $9.99/year with a 7-day free trial), a $49.99 Founder lifetime for a diverse revenue mix, a pro entitlement shared across iPhone and watch, and a RevenueCat-dashboard paywall I can iterate on without shipping app updates. And because AtLeast has no accounts, RevenueCat's anonymous app user IDs are what let monetization survive that promise: purchases and restores work with zero sign-in, and the privacy label stays "not linked to your identity."

Challenges

  • Silence is a feature you have to engineer. Testers said the taps were too polite, but the obvious stronger feedback types can play sound — unacceptable for a silent timer. Getting "keep going" to read on a real wrist without ever breaking the silence promise took a haptic-strength ladder and on-wrist testing, not API defaults.
  • A one-slot radio. WatchConnectivity's application context is a single latest-wins slot that silently drops identical payloads. Making sync rock-solid meant monotonic revisions, idempotent intents, and heartbeats — distributed-systems thinking for two devices in the same pocket.
  • Pro on a watch that never buys. Purchases happen on the iPhone through RevenueCat, but the watch has to unlock too. Running RevenueCat on both devices meant two anonymous customers posting the same receipt, and the entitlement could bounce between them. So the watch dropped the RevenueCat SDK and reads StoreKit 2's Transaction.currentEntitlements itself, caching only answers StoreKit confirmed. The phone sends just a nudge (an entitlement revision riding the same revisioned sync snapshot), and the watch re-checks StoreKit. An unknown state looks exactly like free, with Restore always one tap away.
  • CloudKit has to converge on its own. Timers and history sync through SwiftData's private CloudKit database, which forbids unique constraints, so the same record can arrive twice. Every device collapses duplicates with the same deterministic rule (highest version, then a fixed tie-break), so both land on the same copy instead of whichever arrived first. CloudKit merges per field, so I stopped storing values derived from each other: a session keeps its target instant, not a duration too, so two builds can't write contradictory halves of one row. "This device" vs. "the watch" means opposite things on each side of a shared database, so history stores an absolute device identity. And the device that hosts a session writes its record; nothing is relayed over WatchConnectivity for the other device to write.
  • Eyes-free is a discipline. Every feature request gets tested against "does this make someone look at their wrist?" Saying no to a progress ring is harder than building one.
  • Shipping the last 10% solo: six-language screenshot pipelines, accessibility audits, and App Review prep, as a solo developer and parent of six.

What I learned

The absence of feedback can be the product. Every platform pushes you toward more — more alerts, more numbers, more engagement. Building an app whose success state is silence meant fighting that current at every level, from API choices to App Store screenshots. I also learned that the "floor, not ceiling" shape fits far more of life than breathing exercises: minimum walks, minimum reading, minimum practice. Once you see floors, you can't unsee them.

What's next

Siri and Shortcuts (start and stop a session by voice), a Focus filter, session history insights, and growing Pro on the "memory" promise without ever compromising the free, silent core.

Built With

  • revenuecat
  • swift
  • swiftui
  • watchconnectivity
  • watchos
Share this project:

Updates

Submission history