Tech stack
Platform#
| Language | Kotlin (JVM target 17) |
| Minimum Android | 8.0, API 26 |
| Target / compile Android | API 36 |
| UI | Jetpack Compose with Material 3 |
| Build | Gradle Kotlin DSL, version catalog, KSP |
| Modules | One: :app |
The API 26 floor is what the platform APIs Haven relies on require — the JCA signature provider used to verify blocklist updates, and the notification-channel model for the ongoing protection notification.
Libraries, and why each one is present#
| Area | Library | Why |
|---|---|---|
| UI | Compose, Material 3, Navigation Compose | The whole interface, including Sam, who is drawn on a canvas rather than shipped as assets |
| DI | Hilt (+ Hilt Navigation Compose, Hilt Work) | One object graph shared by activities, services, receivers and workers |
| Persistence | Room | The block rules, block log, accountability events, financial pause log and learned sites |
| Preferences | DataStore (Preferences) | Fourteen small, separate settings stores |
| Async | Kotlin Coroutines + Flow | Reactive settings caching, the DNS loop, background writes |
| Serialization | kotlinx.serialization | Blocklist payloads, API bodies, and the JSON blobs stored in DataStore |
| Background work | WorkManager | The periodic blocklist sync and the widget refresh |
| Networking | Retrofit + OkHttp + the kotlinx-serialization converter | The blocklist API and the Supabase APIs |
| Auth (device) | AndroidX Credentials + Google Identity | Google sign-in via Credential Manager |
| Lock | AndroidX Biometric + Fragment | Optional biometric acceleration of the PIN gate |
| Billing | RevenueCat (purchases + purchases-ui) | Subscriptions, the paywall and the customer center |
| Widget | Jetpack Glance | The Sam widget, rendered to RemoteViews |
| Testing | JUnit, MockK, coroutines-test, Turbine | 70 unit-test files, concentrated on domain/ |
Deliberately absent: any analytics, crash-reporting, advertising, attribution or remote-config SDK, and any push messaging library. See Data handling.
Cryptography#
All from the platform, with no third-party crypto library:
- Blocklist authenticity — ECDSA P-256 with SHA-256, verified against a public key pinned in the app. Rotating the server key requires shipping a new app version; that is the intended trust boundary.
- PIN and password storage — salted PBKDF2. Raw secrets are never persisted.
- Pairing codes —
SecureRandom. - Google ID tokens — verified on-device against Google's published keys, checking signature, audience, issuer and expiry before the token is trusted.
Build configuration#
Configuration that differs per environment is injected at build time from a file that is kept out of version control. The app degrades gracefully when a value is absent rather than failing at runtime:
| Absent | Behaviour |
|---|---|
| Google sign-in configuration | The Google button reports itself as not configured |
| Backend account configuration | Accounts fall back to local-only accounts |
| Billing configuration | The app falls back to a local mock paywall |
No key material of any kind is present in this documentation, in the repository's tracked files, or in the published site.
Release hardening#
minifyEnabledandshrinkResourceson for release.allowBackup="false"— the app's data is not included in Android backup.- All cleartext traffic denied in the release network-security configuration, with no exceptions. The local-development allowance exists only in the debug variant's own configuration file and is never packaged into a release build.
- HTTP logging is off in release builds.
- A build-time guard fails a release build if a test-store billing key is configured, so a shipped app can never bill against a test store.
- The release build is signed from credentials that live outside version control; without them the build simply stays unsigned, so the project still builds for anyone without the key.
Language handling#
Haven picks its own interface language, independent of the phone's. That has one non-obvious build consequence: Play splits an app bundle per language and installs only the splits matching the device's locales, so a language chosen in-app would silently render in the wrong language on a phone set to something else. Language splitting is therefore disabled and every language ships in the base module.
Strings that are rendered outside any of Haven's own activities — the device-admin dialogs, the widget, and partner emails composed from a background component — explicitly apply the app's chosen language, because those contexts would otherwise follow the phone.