Case study
Fathom
iOS focus app built around fishing: cast a line, put the phone down, and the longer you stay away, the deeper the line sinks.
- TypeScript
- React Native
- Expo
- Skia
- Swift
- Supabase
- PostgreSQL
- 0 / 22leave-the-app runs misread as a phone lock in on-device testing
- 1,281unit tests on the main branch, plus 13 database suites
- 18Postgres tables, each with row-level security
- 14screens, from voyage setup to friends and messages

Overview
Fathom is an iOS focus app built around a fishing metaphor. You pick a zone and a trip length, cast off, and put the phone down. The line sinks in real time, and everything you catch comes from time actually spent away from the phone.
- Core loop: set up a voyage of 10 to 120 minutes, watch the line descend through four depth bands, then reveal the catch one fish at a time.
- Leaving the app: locking the phone is fine, but switching apps starts a 30-second fuse. If the line snaps, you keep what you've caught and lose only the completion bonus.
- Collection: keep or sell each fish in an aquarium; coins buy rods and bait.
- Social: add friends by code, visit their islands, and send gifts and messages in a bottle.
- Design rules: six written rules govern the product: no reward notifications, no streaks, money never buys outcomes, and failure costs a bonus, never the catch.
My contribution
A solo project. I set up a Claude Code subagent workflow for the implementation and owned everything that decides what gets built and whether it works.
- Product: wrote the product spec and the six design rules, and settled every design question the agents raised.
- Architecture: designed the server-side integrity model, the offline sync queue, and the crash-safe write order for every coin path.
- On-device validation: ran a lock-detection spike on my iPhone before the session runtime was built, and every lock, force-kill, and crash test since.
- Review: ran a whole-branch review at the end of each development cycle. Across ten cycles, it caught a critical or important issue that per-task checks had missed every time.
Implementation
An Expo and React Native client in strict TypeScript, a Supabase Postgres backend, and one native iOS module.
- Lock vs. leave: a Swift module listens for iOS lifecycle and protected-data events and buffers them with true timestamps while JavaScript is suspended. A classifier then labels each background episode as a lock (allowed) or a leave (starts the fuse), without needing Screen Time permissions.
- Server-side integrity: the only write path for voyages is a Postgres function that credits the smallest of the claimed, committed, and server-metered minutes. Total credited time can never exceed real server time since the account was created, so changing the phone's clock earns nothing.
- Offline-first: all game and economy logic is pure TypeScript with a seeded random number generator. State lives in atomic JSON files on the device, and a queue of pending changes syncs on launch, on foreground, and after each voyage, setting a change aside after 8 failed attempts.
- Data: every table has row-level security: players see only their own rows, and friends see only showcase data such as a profile and aquarium.
- Rendering: the descent, island, aquarium, and reveal scenes are drawn procedurally with Skia, with parallax running on the UI thread through Reanimated.

Evaluation
- Lock-detection spike: 48 runs on an iPhone (280 recorded events) before the runtime was built. The lock signal arrived 0–118 ms after the app went to the background, and none of the 22 leave runs was misread as a lock.
- Tests: 1,281 unit tests on the main branch, 13 database integration suites (including clock-skew tests against the real voyage function), and 7 golden snapshots that pin the save-data format.
- Rule-enforcing tests: some tests scan the source code to enforce the design rules, for example that only one file may send notifications and that nothing implements a streak.
- Economy: a pacing simulation set prices against measured income of about 103 coins per 45-minute session, so the first rod upgrade costs about 2.3 sessions.
Challenges
- Lost lock signals. If the phone locked while the app was already suspended, the lock event never arrived. The spike showed that a data-available event at resume reliably recovered those cases, so the classifier upgrades the episode to a lock after the fact.
- False warnings. The first device run fired the line-about-to-snap warning during locked sessions, because the lock signal lands up to 118 ms after backgrounding. Flushing native events on the lock signal itself fixed it.
- Force-kill bypass. Killing the app could skip the fuse entirely. Flushing events as the app enters the background closed the gap.
- Crash-safe coins. Every coin path writes in a fixed order so a crash at any point replays cleanly, and purchases grant the item first, so a crash favors the player. Tests inject a crash between every pair of writes.
- Bait that did nothing. Whole-branch review found a bait type that raised a fish's coin value while sales paid a different field, so it had no effect. The fix made it scale the sale price.
What I learned
- Test risky platform behavior on real hardware before building on it: the spike found a lost-signal case the design had to handle, and the first device run found a false-warning bug.
- When agents write the code, the leverage is in the spec, the rules, and the review. Written design rules, and tests that enforce them, kept 300+ commits consistent.
- Trust the server's clock, never the device's.