Read the App Store reviews for any medication tracker. Filter to 1-star. Count the complaints. By a wide margin, the #1 reason a user uninstalls a health-tracking app is:
"Notifications don't fire. I missed a dose."
Notifications are the most boring feature in the app. They are also the most important. A health app whose reminders don't fire is worse than no app · the user replaced their working system (a phone alarm, a sticky note, a partner's reminder) with a thing that failed silently.
Regimio's reminder reliability target is 50 firings in a row, zero drops. Here's how we engineer it.
Push vs local · why we don't use a push server
Most apps use server-pushed notifications. The flow:
- Schedule a reminder on a backend cron
- Backend hits APNs (Apple) or FCM (Google) at the scheduled time
- Service delivers to the user's device
- App receives the notification and displays it
The failure modes:
- Backend cron service goes down · no reminders for an hour
- APNs / FCM rate limits hit during a deploy storm · silent drops
- User's network is offline at delivery time · delayed or dropped
- User's phone is in deep sleep on Android · delayed
- Backend's clock drifts · fires at wrong time
- App's push token expires · invisible until the next foreground
Plus the privacy cost: a server has to know your schedule, your compound names, your dose times · to know what to send and when.
Local notifications skip the entire stack. The flow:
- App schedules the notification on-device via
expo-notifications - iOS or Android scheduler fires it at the scheduled time
- App receives the firing and displays it
No backend. No network. No push token expiry. No timezone math across a remote server.
For Regimio, this is also the privacy win: the OS scheduler doesn't tell anyone what your reminder is for. It just fires.
The hard parts of local
Local notifications aren't a perfect solution. There are three real engineering challenges:
1. The OS can defer notifications when your app is in deep sleep.
Android is especially aggressive about battery optimization. A notification scheduled for 7:00 AM may fire at 7:08 AM if the system was in Doze mode and the user's phone wasn't being actively used.
Mitigation: Regimio's notifications are flagged with high priority + AlarmManager.setExactAndAllowWhileIdle on Android. They fire to the second 99% of the time in our internal tests.
2. App lifecycle bugs can cause "ghost" notifications.
Edge case: user reschedules a notification (e.g., changes their morning dose from 7:00 AM to 7:30 AM). The old notification is canceled by ID, the new one is scheduled. But on iOS, if the cancel didn't complete before the new schedule, you can end up with two reminders firing at different times.
Mitigation: Regimio uses a single notification ID per (compound, day) and overwrites in place. Editing visually confirms save with a toast + checkmark · so users who reschedule see the saved state, not a silent action.
3. Reboot edge cases.
On Android, scheduled notifications survive a reboot but require a BOOT_COMPLETED receiver. If the app hasn't been opened post-boot, the user can miss a dose window.
Mitigation: We include a BOOT_COMPLETED receiver that re-reads the SQLite schedule and re-registers any notifications scheduled for the next 48 hours.
The audit pattern
Every release runs through a notification-audit checklist:
- Schedule 10 notifications across compounds, doses, and timing patterns
- Force-stop the app
- Restart the device
- Wait through each scheduled window
- Verify all 10 fire within 60 seconds of scheduled time
Failures are P0. We don't ship a release where this isn't 10/10.
Auto-derived reminders
The interesting ones aren't the dose reminders. They're the auto-derived notifications Regimio computes from your protocol:
Trough-day blood draw. For TRT compounds, the app calculates a window 12–24 hours before your next injection. Pushes: "Trough window · draw blood tomorrow morning before your shot." See the trough draw post for the math.
BAC water 28-day expiry. When you log a vial puncture, two notifications are scheduled: one 3 days before expiry, one on expiry day. Both reference the specific vial: "BPC-157 vial expires Friday. Reorder or discard."
Refill warning. After every dose log, the app recomputes days-until-empty for your active vials. At 7-day runway, you get: "Tirzepatide will run out Sunday. Reorder this week."
Protocol cycle off. For cyclic protocols (CJC + Ipa, 12 weeks on / 4 weeks off), the cycle-off date is auto-scheduled when you create the protocol. Pushes: "GH Pulse cycle ends today · start your 4-week off period."
None of these require a server. All of them are scheduled by reading the SQLite data and computing the trigger time.
The early-dose reset
A static reminder schedule is the wrong abstraction for real-life dosing. Every TRT and peptide user pushes a dose by an hour or two sometimes.
The rule: logging a dose early resets the cadence anchor.
You normally inject Mondays. You inject Wednesday this week because of travel. Your next reminder fires the following Wednesday · not the originally-scheduled next Monday.
Implementation:
function rescheduleAfterDoseLog(compound, doseLog) {
const nextDoseTime = doseLog.taken_at + cadenceIntervalMs(compound);
cancelNotification(`${compound.id}-next`);
scheduleNotification({
id: `${compound.id}-next`,
title: `${compound.display_name} · due in ${formatRelative(nextDoseTime)}`,
triggerTime: nextDoseTime,
});
}
The cadence anchor moves with the user. The reminder follows.
Edit confirms save
The other reliability bug: editing a reminder that silently doesn't save. The Mounjaro Tracker review pattern:
"Try to edit the reminder times and it never saves it."
Regimio's fix: every reminder edit visually confirms. A green toast slides up:
✓ Reminder saved · BPC-157 daily · 8:00 AM
Plus the home screen reflects the change immediately. If the toast doesn't show, the save didn't happen · the user knows to retry.
What about Apple Watch / Wear OS?
v1 plan: complications on watchOS that display the next-dose countdown. Tapping deep-links to the phone app for logging.
v2 plan: independent watch app for logging when the phone isn't reachable.
For v0, the lock screen widget on the phone is the priority. Insight #56 from our user research: the widget is used more than the app itself. We're prioritizing widget reliability over watch integration.
Why this is a marketing point
We mention notification reliability on every product page. Not because it's flashy, but because the existing market has it as the #1 user complaint. If we don't talk about it, users assume we're the same as the rest.
Saying "50 firings, 0 drops" in the press kit is a specific, falsifiable claim. If a user catches us missing a notification, we own the bug publicly via the changelog and fix it in the next release.
The reminder design and trough-draw timing walks through the math. The local-first architecture post covers the rest of the stack.