"All health data stays on your device" is one sentence. Making it true is several thousand lines of code, a few firm architectural rules, and a lot of saying no to features that would have been easier to build with a backend.
This post is the engineering story behind that sentence. What we kept on device, how we keep it there, and the design choices we made to prevent the slow drift that takes most privacy promises apart.
The core rule
There is exactly one architectural rule that matters. Every other decision follows from it.
No mylune network request ever carries health content. Not a cycle date, not a symptom, not a note, not a flag, not a derived statistic, not a UI event triggered by a health interaction.
The rule is written, taught, code reviewed, and grep tested. Every new request goes through a single network layer that whitelists destinations and payload shapes. If a payload field is not on the list, the request fails in development before it can ship.
We could have stopped at "we promise not to send health data." Instead the architecture refuses to send it. The difference matters because promises drift. Architectures do not.
Where the data lives
On iOS, the database is SQLite with the Data Protection class set so the file is unreadable when the device is locked. Sensitive keys, including the PIN hash and any encryption keys, live in the iOS Keychain with an access policy that requires the device to be unlocked once after boot and forbids iCloud sync.
On Android, the database is SQLite under file based encryption. Sensitive keys live in the Android Keystore, hardware backed where the device supports it. The encryption key for the database itself is generated on first launch, stored only in Keystore, and never exported.
Day to day reads and writes are also wrapped with MMKV for fast preferences, also encrypted, also local. None of these layers touch the network.
What we cut to make the rule possible
The rule of "no health content in any network request" eliminates a lot of features other period apps build. We chose not to build them, rather than build them and hope nobody looked too closely.
- No cross device sync. A user with two devices sees two separate logs. We considered an end to end encrypted sync option and decided to ship without it in V1, because the simplest way to keep a promise is to remove the place where it could break.
- No cloud backup. The same reason. We export to local files instead.
- No "predicted for you" features that rely on aggregate user data. All predictions are computed locally from the user's own history.
- No A/B testing in the app. There is no experimentation framework, no flag service phoning home, no remote config. Feature flags are compiled into the binary.
- No analytics. No usage events, no funnel tracking, no anonymous identifiers, no third party SDK. We do not know which screens are visited or which features are used. We accept that as the cost of being able to say so.
- No crash reporting SDK. Crash signals are extremely hard to anonymise. If a stack trace mentions a screen called "LogPeriodView," that itself is a health signal. We do not run a crash collector.
We are aware that these choices make the engineering job harder. Without analytics, debugging behaviour at scale is slower. Without crash reporting, fixing rare bugs takes longer. We accept that, because the alternative weakens the only promise that distinguishes the product.
The export pipeline
Users can generate a doctor visit PDF, an export of their data, or a backup snapshot. All of these are health content. None of them ride a network request to a mylune server.
The export pipeline generates files entirely in memory, writes them to the operating system's temporary directory, and hands them to the system share sheet. When the share sheet closes, the temp file is deleted. Nothing is logged about it. Nothing is sent to us. The file lives only on the user's device, and only as long as the user wants it to.
The share sheet then routes the file wherever the user chooses. If they email it to their doctor, that email is between the user's mail provider and their doctor. mylune is not in the loop.
What about the website
mylune.com is a separate system. It is a static marketing site. It hosts a waitlist email form, a contact form, and a few editorial pages. None of those services touch any in app data.
The waitlist email form sends an email address and nothing else. It is processed by a transactional email provider and used to notify the person when the app is ready. The address is stored separately, with no link to any app data, because there is no app data to link to.
If a user uses the same email address inside the app at some future point, mylune would still not be able to connect the two systems, because the in app data has no concept of identity and is not transmitted off device.
The "even by accident" problem
Most privacy violations are not malicious. They are leakage. A logging library that records URL paths. A debug build with verbose telemetry left enabled. A third party SDK that quietly phones home. A future product manager who thinks adding a single tracking pixel will not really count.
We try to defend against all of these. The defences are not glamorous, but they are the actual work:
- One network layer, with a destination whitelist.
- One logging layer, with a payload allow list. Logs contain version numbers and error types, never user content, screen names with health context, or symptom values.
- No third party SDKs in the production binary. We periodically grep the build output and the iOS and Android dependency trees to confirm.
- A test suite that catches forbidden network calls in development. A request to an unexpected destination during a unit test fails the test.
- A CI gate that fails the build if the production binary contains any string from a list of known analytics endpoints.
These are unglamorous, easy to skip in a hurry, and the reason most privacy claims drift over time. We try not to skip them.
What we did not solve
We do not pretend on device storage is a complete privacy story. It does not protect against device level adversaries who already have the user's phone unlocked. It does not protect against malicious operating system updates. It does not protect against the user choosing to share an export with someone who then shares it further.
What it does is remove mylune as a meaningful intermediate party. The set of people who can read your data is the set of people who have physical control of your phone and your unlock factors. That is the smallest realistic set we can build a product around.
The takeaway
A serverless health app sounds, from the outside, like a downgrade. From the inside, it is a different design constraint that produces a different product. Some features genuinely become hard or impossible. Other features become better, because the architecture forces clarity about what is local and what is not.
We think the trade was right for V1. The whole product premise stands on one promise. Making that promise verifiable was worth the engineering cost.