Angel Campa
Colour theme

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
Source
period-tracker-app-floriva-snapshot

Built with

  • Expo 54
  • React Native 0.81
  • TypeScript
  • Expo Router
  • expo-sqlite
  • Drizzle
  • @noble/ciphers
  • Detox
Cycle calendar for August 2026 with a key for period, predicted, fertile, logged, spotting and selected days. A banner above the grid reads "Next period expected Aug 28 to Sep 2", marked medium confidence, and beneath it "Based on 4 local cycle starts".
The calendar states its own confidence and what that confidence rests on. Captured from the 1.4.0 release build by the Detox harness against a seeded fixture, with the status bar normalised.

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 drizzle

Measured

History
706 commits, 2026-04-08 to 2026-08-11
Provenance

Source docs/metrics.md § History

Measured

Screens

A screen titled "How Floriva predicts", explaining that predictions stay on this device and adapt as more entries are logged, that Floriva shows estimates and not medical certainty, and that irregular-cycle support keeps predictions broader when timing varies.
Where the number came from, built as its own screen rather than a footnote. Same 1.4.0 capture sweep.
Insights screen headed "What your last three cycles say", showing cycle length described as varying widely across bars of 28, 34 and 21 days against a 25-day average, a phase rhythm breakdown of period, fertile and luteal lengths, and a monthly briefing summarising 5 local logs.
Three cycles at 28, 34 and 21 days, reported as varying widely rather than flattened into one tidy average.
Import screen offering three sources: a Clue JSON export, a Flo JSON export, and manual entry of period starts for the last 12 months. A closing note reads that imports only open the file you choose, and that Floriva does not scan your storage or upload that file.
Import previews what it parsed before it writes anything to the database.
Export backup screen with a backup passphrase field, a confirmation field and a Create backup file button, above two notes: that Floriva encrypts the file before you move it off this device, and that Floriva cannot recover this passphrase.
The one genuinely encrypted artifact in the product, sealed with a passphrase only the user holds. Floriva cannot recover it for you, by design.
Read the repositoryperiod-tracker-app-floriva-snapshot

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