Building a Health App That Cannot Read Its Users
Pavlo Rubanovskyi
June 12, 2026 · 7 min read

The constraint we started from
Most health apps ask you to create an account first and explain the privacy later. When we built Kvit, our period and pregnancy tracker, we inverted that: the app had to be useful before it knows anything about you, and it had to stay useful if our servers disappeared tomorrow.
That is not a marketing position. It is an architectural constraint, and it decides almost everything downstream.
What local-first actually costs
Saying "your data stays on your phone" is easy. Living with it is not. Here is what the decision took away:
- No server-side analytics on user content. We cannot query "how many users log symptom X" because we do not have the data. Product decisions come from aggregate, non-identifying signals only.
- No password reset. There is no account to reset. Lose the device without a backup and the entries are gone — which is the honest consequence of a key that never leaves the phone.
- No cheap cross-device sync. Sync has to be built as an encrypted payload exchange, not as a database read.
We accepted all three. For a category where a leak is not an inconvenience but a personal safety issue, that trade is the product.
The encryption, named precisely
Vague claims are worse than none. "Bank-grade security" tells a user nothing and tells an auditor less. The mechanism, exactly:
1. On first launch the app generates a 256-bit key with a cryptographically secure random source. 2. The key goes into the platform's hardware-backed store — Keychain on iOS, Keystore on Android — and never leaves the device. It is not in our code, not in a config file, not in a backup we can read. 3. The on-device database is encrypted with AES-256 using that key. Entries are encrypted before anything approaches the network.
`dart final key = await secureStorage.read(key: 'db_key') ?? await _generateAndPersistKey(secureStorage);
final db = await openEncryptedDatabase( path: await _databasePath(), encryptionKey: base64Decode(key), ); `
The important line is the second one. Encryption with a key stored next to the data protects nobody. The whole guarantee rests on where the key lives.
No ad SDK, and why that is a security decision
Kvit ships without an advertising library. That is usually framed as a UX choice, but in a health app it is a data-flow choice: an ad SDK is a third-party binary with network access running inside your process. Every one you add is a party you now have to vouch for.
There is nothing to hand an ad network, because there is no ad network in the binary.
What this means if you are commissioning an app
If you are building in health, finance, or anything a user would not want read aloud, three questions are worth asking your engineering partner early:
- Where does the encryption key live? If the answer is not "the platform's hardware store", the encryption is decorative.
- What works with the network off? Local-first is not a feature you add later; it changes the data model.
- Which third-party SDKs run in-process? Each one is an additional privacy promise you are making on someone else's behalf.
We built Kvit to be able to answer those three without hedging. It is the same standard we hold client work to, and the same reasoning that keeps Qivvo's receipt scanning on the device.