Security8 min read
Privacy-First Flutter Apps: On-Device Data
Build a privacy-first Flutter app that keeps sensitive data on the device with no account and no cloud sync, using local storage and an app lock.
- #flutter
- #privacy
- #offline-first
- #local-storage
- #security

HerCycle, a privacy-first period tracker I built, stores everything on the device: cycle logs, symptom history, and health reports. There is no account, no registration, and no server sync. The app works fully offline, and it ships with a PIN and biometric app lock. For an app that holds some of the most sensitive health data a person has, that architecture is not a marketing line. It is the product. Most apps treat privacy as a settings screen. A privacy-first app treats it as an architecture decision made before the first screen is drawn. This article shows how to build one in Flutter: on-device storage, no account, no cloud sync, an app lock, and data flows that keep sensitive information inside the phone. The patterns come from HerCycle, but they apply to any app that handles health, finance, or personal data. You can see the finished app in the HerCycle case study.
What privacy-first actually means
Privacy-first is a short list of rules, and every feature has to pass all of them:
- Data lives on the device by default. If a feature needs a server, that is a deliberate exception, not the default.
- No account. No email field, no sign-up screen, no password reset flow. The app is usable thirty seconds after install.
- Nothing leaves the phone without an explicit user action. No silent uploads, no background sync, no analytics events fired on launch.
- The app works fully offline. Airplane mode changes nothing, because there is no server to reach.
- A lost phone does not mean exposed data. An app lock and encrypted storage protect the data even when the device changes hands. Rule 5 is the one teams forget. On-device storage is only private while the phone is in the owner's pocket. The moment someone else picks it up, the database is just a file. That is why HerCycle ships a PIN and biometric app lock: the lock is the feature that makes the rest of the architecture believable. The full lock implementation is covered in Flutter Biometric Auth With local_auth.
The storage layer: pick boring, local tools
A privacy-first app does not need exotic storage. It needs the right local tool for each kind of data:
| Data | Tool | Why |
|---|---|---|
| Simple settings and flags | SharedPreferences | Tiny key-value pairs, built for exactly this |
| Structured records | sqflite or drift | Queryable tables with migrations |
| Secrets such as a PIN hash | flutter_secure_storage | Backed by the OS keystore, not plain files |
| Exported files | App documents directory | Files the user explicitly saves or shares |
| The point of the table is the last column. SharedPreferences writes plain XML on Android, so it is fine for a theme setting and wrong for a PIN. Secrets belong in the keystore. Structured health records belong in a database you control, with a schema you can migrate. | ||
| A cycle log table in sqflite looks like this: |
Future<Database> openCycleDatabase() async {
final dbPath = join(await getDatabasesPath(), 'cycles.db');
return openDatabase(
dbPath,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE cycle_logs(
id TEXT PRIMARY KEY,
startDate TEXT NOT NULL,
endDate TEXT,
flow TEXT NOT NULL,
createdAt TEXT NOT NULL
)
''');
},
);
}
Keep the schema small and typed. Dates go in as ISO strings, not timestamps in a local timezone, so a log recorded while travelling still sorts correctly. Every record gets a locally generated id, not a server id, because there is no server.
No account means designing without a user id
Removing the account removes a whole category of work: no auth screens, no session tokens, no password resets, no "verify your email" flow. It also removes the easiest way to tie records to a person, which is exactly the point. Three design consequences follow:
- Local ids only. Generate record ids on the device with the uuid package. Nothing in the database should be able to identify a person if the file is ever seen outside the phone.
- No silent backup. Check what the operating system backs up automatically. On Android, review the auto-backup rules for your app data; on iOS, review what iCloud backs up. A database that quietly syncs to a cloud backup is a server sync by another name.
- Exports replace sync. Users still want their data safe and portable, so give them a user-initiated export: a file they save, print, or share themselves. HerCycle does this with cycle health reports the user can share with their doctor. The app never uploads a backup on its own. The trade-off is honest and worth stating in the app: if the user uninstalls the app without exporting, the data is gone. A privacy-first app says that plainly instead of hiding it.
Lock the front door: PIN and biometrics
The lock has two parts. Biometrics are the convenient path, and a PIN is the fallback for devices without biometrics or for a failed scan. Both must gate the same thing: the first screen that shows data.
Future<bool> unlockWithBiometrics() async {
final auth = LocalAuthentication();
final canCheck = await auth.canCheckBiometrics;
if (!canCheck) return false;
return auth.authenticate(
localizedReason: 'Unlock to view your data',
options: const AuthenticationOptions(biometricOnly: true),
);
}
The PIN itself is never stored. Store a salted hash in flutter_secure_storage and compare hashes on unlock:
Future<void> savePin(String pin) async {
const storage = FlutterSecureStorage();
final salt = DateTime.now().millisecondsSinceEpoch.toString();
final bytes = utf8.encode(salt + ':' + pin);
final hash = sha256.convert(bytes).toString();
await storage.write(key: 'pin_salt', value: salt);
await storage.write(key: 'pin_hash', value: hash);
}
Never store the PIN in SharedPreferences or in the database. If the storage is readable, the secret must still be useless, and a salted hash is useless without the PIN that produced it.
Analytics and third-party SDKs: default to off
Every SDK you add is a data flow you now own. An analytics SDK sends events, a crash SDK sends stack traces, an ads SDK sends identifiers. In a privacy-first app, the default for all of them is off.
That does not mean flying blind. It means choosing deliberately: if you need crash reports, pick a self-hosted or privacy-respecting option, configure it to strip identifiers, and say so in your privacy policy. If you do not need it, leave it out. The same review applies to fonts, maps, and any SDK that phones home on launch. Open your app behind a network inspector once, watch what leaves the phone on a cold start, and remove anything you cannot justify.
Predictions are estimates, not medical advice
HerCycle predicts periods with an irregular-cycle algorithm that adapts to cycle variations, and estimates ovulation and fertile windows using basal body temperature, cervical mucus, and pregnancy due-date inputs. Those predictions are useful, and they are also wrong sometimes, because cycles vary. A privacy-first health app has to be honest about that in two places: in the interface, where every prediction is labelled an estimate, and in a clear medical disclaimer that the app is not a substitute for medical advice. The disclaimer is not legal decoration. It is part of the trust the whole architecture is built on: the app tells the user what it knows, what it guesses, and what it does not do with their data.
Let users export, never auto-share
Sharing is a feature, not a sync mechanism. HerCycle lets users share cycle health reports with their doctor, and that flow has three properties worth copying:
- It starts with a tap by the user, never automatically.
- It shares a generated file or report, not live access to the database.
- It uses the system share sheet, so the user picks the destination and sees exactly what leaves the phone. The same pattern works for any sensitive app. The Flutter Secure QR Medical Profile Sharing article shows a related approach for medical data, and the health vitals tracker covers logging and trend views for family health data. Reminders deserve the same treatment. HerCycle sends local reminders so daily readings and logs are never missed, using on-device scheduling only. Local notifications need no server and no push token, which is why they fit a privacy-first design. The setup is covered in Flutter Local Notifications: Full Guide.
Test your privacy promise before you ship it
Privacy claims are easy to write and easy to break. Run this checklist on a release build:
- Airplane mode: every feature works, including first launch after install.
- Fresh install: there is no account screen, no email field, no sign-in wall.
- Network inspector: nothing leaves the phone on a cold start except requests you can name and justify.
- Lock check: background the app, reopen it, and the PIN or biometric prompt appears before any data.
- Uninstall: reinstalling starts empty, and the app tells the user that uninstalling deletes their data. If any check fails, the fix belongs in the architecture, not in the privacy policy. A policy that promises what the code does not do is worse than no policy at all.
The short version
A privacy-first Flutter app is a set of defaults: data on the device, no account, no silent sync, a lock on the front door, and sharing that only the user can start. HerCycle proves the shape works for genuinely sensitive health data, and the same shape fits finance, journaling, and any app where the data is the person. Build the defaults first, then add features inside them. Privacy added at the end is a settings screen. Privacy built in from the start is an architecture. Get in touch and let's talk about your project.
Frequently asked questions
- What makes a Flutter app privacy-first?
- Data stays on the device by default, there is no account or server sync, nothing leaves the phone without an explicit user action, and the app works fully offline behind a PIN or biometric lock.
- Which Flutter packages store data on-device?
- SharedPreferences for simple settings, sqflite or drift for structured records, and flutter_secure_storage for secrets such as a PIN hash. None of them syncs to a server.
- Do I still need a privacy policy for an offline app?
- Yes. The policy should state plainly that data stays on the device, that no account is required, and what happens on uninstall, so users can verify the promise.
- How do users back up data without cloud sync?
- Offer a user-initiated export, such as a file the user saves or shares themselves. The app never uploads a backup on its own.


