WorkThe April wave
Floriva
A period tracker for iOS and Android whose data never left the phone it was installed on.
- Ran on
- App Store and Google Play, on-device only
- Ran
- April 2026 to August 2026
- Domain
- Period tracking
Built with
- Expo 54
- React Native 0.81
- TypeScript
- Expo Router
- expo-sqlite
- Drizzle
- @noble/ciphers
- Detox

The decision
Floriva stored everything on the device and made zero network calls anywhere in its application
code: no fetch, no XMLHttpRequest, no WebSocket, no axios call sites in src/, app/,
components/, constants/, or drizzle/, confirmed by grepping the tree rather than asserted. No
account, no analytics SDK, no crash reporter, no ad network.
The only ways data ever left the device were OS hand-offs the user explicitly triggered: a store
purchase, the system review sheet, a mailto: link, or opening the privacy policy in a browser.
Nine of the 22 Detox specs targeted privacy-adjacent surfaces directly (biometric lock, encrypted
backup export and restore, delete-all-data, private timeline, and both import flows) rather than
leaving the app’s central promise to manual QA.
That discipline did not stop at the architecture. In July 2026 the project ran a full self-audit against its own public marketing and found a real overstatement: copy claiming “encrypted sync” and “zero-knowledge” for a product that had no server to sync to and no knowledge to be zero about.
The fix corrected the claim rather than adding encryption after the fact to make it true.
An idempotent, add-column-only repair routine now checks and heals the schema on every boot. It exists because a separate migration file once carried out-of-order timestamps, so an install already at migration ten silently skipped the next three, with no error and no crash, just three missing columns on the accounts that had been around longest. Renumbering the journal would have invalidated the hashes already recorded on every device in the field, so the repair runs forward instead, healing every affected install without touching a hash already on record.
Architecture
app/ Expo Router routes: orchestration only, 1,059 lines total
zero imports from src/lib. The layering rule actually holds there.
src/
features/ Feature flows, each owning its screens and model
lib/ Domain logic: predictions, parsing, notifications, security
db/ Drizzle + expo-sqlite: schema, repositories, validators, repair
theme/ Tokens and primitives
localization/ 8 locales across 16 message modules
The rule was that domain logic never lived in a screen. Seven pure screen-model builders (2,220 lines) sat between features and their React components so screen behavior was unit-testable without rendering, and the entire routing tree carried no domain imports.
The prediction engine (21 modules) was descriptive statistics over a user’s own logged history: MAD-based outlier rejection with a recency-weighted median, ovulation-signal fusion across basal temperature, OPK surge and cervical mucus with an explicit decline-to-guess path when signals conflicted, and a retrospective-only rule that a temperature-derived estimate, only knowable after the fact, could never open a future fertile window.
The one genuinely encrypted artifact in the system was the user-created .floriva backup export,
sealed with AES-256-GCM under a PBKDF2-SHA256 key at 210,000 iterations, with KDF iteration bounds
enforced at decrypt time and an explicit rejection of all-NUL passphrases at the trust boundary.
Biometric lock worked differently: a random marker in the keychain that flipped app state to gate
the lock screen, the same mechanism used everywhere else access was gated in the app.
The zero-network-calls claim held against the compiled build as well as the source tree.
expo-notifications pulls Firebase Cloud Messaging classes into the shipped Android build, but
with no google-services.json in the project, Firebase never initializes and no push token is
ever requested, confirmed by disassembling the release build rather than taking the dependency
tree’s word for it.
By the numbers
Each figure carries where it came from and when it was measured. Open one to see the command behind it, where the repository recorded a command.
- Product code
- 297 files, 50,485 lines
Provenance
Source docs/metrics.md § Code
Measured
- Test code
- 77,987 lines (1.54 lines of test per line shipped)
Provenance
Source docs/metrics.md § Code
Measured
- Test suite
- 287 suites, 4,596 tests, 0 focused
Provenance
Source docs/metrics.md § Tests
Measured
- Coverage
- 98.80% line, 99.70% function and 90.67% branch coverage across 243 measured modules
Provenance
Source docs/metrics.md § Coverage
Measured
- End-to-end
- 22 Detox specs, 44 scenario declarations across mutually exclusive presets, iOS and Android
Provenance
Source docs/metrics.md § End-to-end
Measured
- Data layer
- 20 Drizzle migrations, 15 SQLite tables
Provenance
Source docs/metrics.md § Data layer
Measured
- Localization
- 8 locales, 1,107 string leaves each, 0 key drift
Provenance
Source docs/metrics.md § Localization
Measured
- Screens
- 72 route files mapping to 51 distinct screens
Provenance
Source docs/metrics.md § Surfaces
Measured
- Network calls in application code
- 0
Provenance
Source README § There is no networking code in the app
grep -rE "\bfetch\(|XMLHttpRequest|new WebSocket|axios" src app components constants drizzleMeasured
- History
- 706 commits, 2026-04-08 to 2026-08-11
Provenance
Source docs/metrics.md § History
Measured
Screens




Its portfolio/ directory holds the architecture, metrics and testing documents these figures were taken from.