Skip to main content

Encryption-at-rest

The SQLite database is encrypted using SQLCipher via sqlcipher_flutter_libs, which replaces sqlite3_flutter_libs in pubspec.yaml. All data written to disk - tasks, check-ins, events, mana state - is encrypted at rest. The feature flag table is not sensitive and is also encrypted as part of the same database. The app bundle itself is not encrypted.

Key management

On first launch, a 256-bit key is generated using the platform’s cryptographically secure random number generator and stored in the platform keystore:
PlatformStorageSetting
iOSKeychain via flutter_secure_storageaccessibility: afterFirstUnlock
AndroidAndroid KeystoreDefault flutter_secure_storage settings
afterFirstUnlock means the key is accessible after the user’s first unlock since boot, supporting background tasks and notifications without requiring biometric authentication on every access. The key is never written to iCloud, Google Drive, or any cloud backup. This is enforced by the storage accessibility settings.

Device restore

If a user restores a device from a cloud backup, the database file may be restored but the encryption key will not be. The database is unreadable without the key. Users are informed during onboarding:
“Your data is stored only on this device. It isn’t backed up to iCloud or Google Drive. Use the export feature to keep a copy.”

Lost key / biometrics change

If the key is lost (device wipe, Keychain reset, failed migration), the database cannot be recovered. The user must wipe and start fresh. This is mitigated by the Export/Import feature.

Corruption detection

If the database fails to open or a query returns an integrity error, classify it as corrupted_local_state per the Error Taxonomy. Surface a calm message:
“Something went wrong with your data. If this keeps happening, contact support and we’ll help.”
Attempt a full rebuild from the event log before surfacing the error to the user. If the rebuild also fails, surface the message and offer the delete-all-data flow as a recovery path.

Delete all data

Accessible from Settings. Deletes everything stored on the device. There is no network call - all data is local.

Flow

  1. User taps “Delete all data” in Settings
  2. Confirmation screen appears with clear, explicit copy (see below)
  3. User taps the confirmation button
  4. Wipe executes atomically

What is wiped

  • SQLite database file
  • Encryption key in the platform keystore
  • All in-memory cached state

What is not wiped

  • shared_preferences (settings and feature flags) - these intentionally survive. Notification preferences, theme, and accessibility settings are not health data and should not reset with the database.
The wipe is atomic: either the database and key are both deleted, or neither is. No partial deletion.

Copy rules

The confirmation screen uses calm, factual language. No guilt, no urgency, no “are you sure?” framing. Primary copy:
“This will permanently delete all your data on this device. Your app settings will be kept. This cannot be undone.”
Confirmation button:
“Delete everything”
Cancel:
“Go back”
Avoid:
  • Exclamation marks
  • Questions (“Are you sure?”)
  • Conditional urgency (“Warning:”, “Caution:”)
  • Blame framing (“You are about to…”)
After the wipe completes, the app returns to the onboarding flow as if freshly installed.

Offline behaviour

The delete flow works fully offline. No network request is made. This is intentional - data deletion must never be blocked by connectivity.