Data storage
Everything on this page lives on the device. Nothing here is synchronised, backed up to a server, or transmitted. allowBackup is false, so Android's own backup does not carry it off the phone either.
The database#
One Room database, currently at schema version 5, with five tables:
| Table | Holds | Written by |
|---|---|---|
| Block rules | Every rule the matcher uses: synced operator domains, wildcards, keyword tiers, encrypted-DNS resolvers, the allowlist, and the user's own filters | The bundled seed, the sync worker, the user, the content watch |
| Block events | The transparency log: which host was blocked, when, by which layer, and the matched rule | The DNS layer and the content watch |
| Accountability events | The history of protection changes | The accountability manager |
| Financial pause events | Which guarded app was paused, when, and whether the user proceeded | The financial pause screen |
| Learned sites | Domains the risk engine taught itself, with the score, the reasons, a structural fingerprint, first/last seen and a hit count | The content watch |
Two design points#
User rules and synced rules share one table but not one authority. The user's personal filters sit under their own rule kinds, and the sync path maps incoming deltas through an explicit allow-list of kinds that does not include them. A remote update therefore cannot write or delete a rule the user made.
Migrations are real, not destructive. The version-4-to-5 upgrade adds the learned-sites table with a written migration rather than falling back to dropping the database, so an app update keeps the user's block log, financial pause history and personal rules intact. For an app someone has been using for months to stay quit, wiping that history on an update would be a real loss.
Preference stores#
Fourteen separate DataStore files rather than one. The separation is deliberate: it keeps concerns that must never influence each other physically apart, and it means account data can be erased without touching protection settings.
| Store | Holds |
|---|---|
| Protection | Whether protection is on, the cooldown length, a pending disable request and its reason, the applied blocklist version, the last security check, first-run markers |
| Lock | The salted PIN hash and whether biometric unlock is enabled |
| Financial protection | Whether the layer is on, the guarded packages, the hard-lock date, the recurring schedule, the emergency timestamp |
| Accountability | The partner's name and email, the pairing code, and whether they confirmed |
| App detection | Packages the user released as false positives |
| Learned site | Hosts the user overturned, which the content watch then leaves alone entirely |
| Risk | Whether the content watch is on, its thresholds, and per-source toggles |
| Budget | The monthly plan and the balance ledger, as JSON |
| Buddy | Sam's treat jar, growth, equipped cosmetics and chosen trick, as JSON |
| Progression | The cached streak and best streak |
| Subscription | The cached Pro entitlement and since-when |
| Personal | The onboarding goal and chosen start profile |
| Language | The chosen interface language, plus a synchronous mirror read at startup |
| Appearance | Light, dark or follow-system |
| Auth | The session and, for local accounts, the credential record |
A note on the "personal" store: it exists specifically so that soft, personal choices can never end up in the same place as anything that decides whether blocking runs.
Deliberately not persisted#
Some state is held in memory only, and would be wrong to persist:
| State | Why it must not survive a restart |
|---|---|
| That the user just proceeded through a financial pause | A grace period should not survive a reboot |
| That the user just asked to uninstall a flagged app | The same; after a restart a still-installed gambling app is simply blocked again |
| Queued partner emails | A message about something that happened last week is noise |
| Warned-host memory in the content watch | Escalation should reflect this browsing session |
| Skipped setup steps | Android silently revokes the accessibility permission on every app update, so a persisted "don't ask again" would turn the strongest layers off for good |
What is never stored anywhere#
- Page text. Read in memory during a scan and discarded. Only a matched domain name is written down.
- Raw PINs or passwords. Only salted hashes.
- A browsing history. The block log records what was blocked, not what was visited.
- Financial account data. Haven holds no banking credentials and connects to no bank.
Erasing it#
| Action | Effect |
|---|---|
| Delete a personal filter | Removes that rule |
| Release a learned site | Removes the rule; the audit entry can be forgotten too |
| Release an app in App-check | Stops flagging it |
| Remove the accountability partner | Clears the partner store |
| Delete the account | Erases the account store locally and the server-side row |
| Uninstall Haven | Removes every table and every preference store with the app |