davekoons.com / case studies / local-first-android-apps
Android apps that can’t use the internet, on purpose
Ebb tracks menstrual cycles. Nexpill tracks medication timing for caregivers. Both handle health data, and both make the same promise: we never receive it. The interesting part is how that promise is kept.
- Role
- Sole developer, SuperDaveLab
- Licence
- GPL-3.0, source on GitHub
- Stack
- Flutter, Dart, Android, SQLite (sqflite), exact alarms, GitHub Actions
A promise the operating system enforces
Plenty of apps say your data stays on your device. The trouble is that a promise in a
privacy policy is only as good as the next update. So both apps go further: their
release builds ship without Android’s INTERNET permission.
Android itself stops them from sending anything anywhere, and anyone can check that in
the app’s permission list.
That one decision has consequences all the way down:
- No account, no server, no sync, no telemetry. Everything is written to a SQLite database on the phone.
- No analytics, crash reporting or ad SDKs, because those are exactly the channels that leak health data.
- Excluded from cloud backup (
allowBackup="false"plus data extraction rules), otherwise Android would quietly copy the database to the user’s cloud drive. - Unused plugin permissions stripped, so the permission list reads as exactly what the app does.
- Every dependency open source, including the transitive Android ones, to F-Droid’s rules: no Play services or Firebase.
- Checked in the release pipeline. The release script checks the built APK’s permissions against an allow-list, so a new dependency can’t slip
INTERNETback in unnoticed.
Private isn’t the same as secret
Both apps spell out a distinction early: private means we never receive the data, not that the app hides from the people around you. Neither app disguises its name or icon, blanks its notifications, or demands a PIN by default. A reminder that says only “something is due” is a worse reminder for a tired caregiver at 3 a.m.
Nexpill: reminders that must be exact
Nexpill began as a React progressive web app. It was ported to Flutter for one reason: web reminders couldn’t be relied on to fire on time, and a late medication reminder is a wrong one. The Android app schedules exact alarms on the device, so they fire on the minute with no signal and no server.
The timing rules are where the care went:
- Interval, set-time, as-needed and date-driven taper schedules, across multiple patients.
- Intervals are real elapsed time: “every 8 hours” stays 8 hours across a daylight-saving change, while set times follow the wall clock.
- Reminders are replanned wholesale from the timing engine after every change, never scheduled piecemeal, so a reminder fires exactly when the engine would say the dose is due.
- Dose history is an audit trail. A mistaken entry is fixed by a correction that supersedes it, not by silently editing the past.
Porting without changing the answers
Rewriting a timing engine in a new language is an easy way to introduce subtle bugs. So before porting, I recorded 2,114 answers from the original TypeScript engine: interval, as-needed, fixed-time and taper schedules, corrections, ties, and boundaries to the minute and second. They were generated in five time zones and are identical in all of them. The Dart engine is tested against those golden answers, and CI runs the suite in several time zones. When I deliberately introduced porting mistakes, each one failed dozens to hundreds of cases.
Ebb: predictions that admit uncertainty
Cycles vary, so Ebb predicts the next period as a range with its confidence rather than a single date. The method is deliberately plain: a shrinkage-weighted median of recent cycle lengths. With no history it says so and assumes a typical cycle; with more history the estimate becomes the user’s own rhythm; if cycles vary a lot, the window widens and the app says as much. It was chosen because it can be explained, not because it’s clever.
Moving history to a new phone without a network was its own puzzle. Ebb sends it as a sequence of QR codes that the other phone scans with its camera. Nothing touches the internet.
What this means for your project
Most apps don’t need to be this strict. But if yours handles health, financial or other sensitive data, or needs to work reliably without signal, the techniques carry over: permission-level guarantees, on-device scheduling, offline data transfer, and test suites that pin behaviour down before a rewrite. I can build that for Android, or bring the same discipline to a system that does talk to a server.