Personal · iOS (Expo)
HitList: offline-first street-spot scouting app
An iPhone app for logging street snowboard and skate spots with cold hands and no signal: capture first, add the details later, everything stored on the phone.
Product owner and user · Feb – Oct 2026
- tests in 37 suites, and the phone still found a crash
- 374
- fields on a spot, specified Oct 16 after using it, not yet built
- 20+ → 7
- Expo upgrade in three one-version hops, each one green
- SDK 54 → 57
The Problem
I ride street spots on a snowboard and a skateboard. A spot is one rideable feature, a rail, a ledge or a bank, at one place. I scout with cold hands, little time and often no signal, and a spot I don't log is easy to forget. I wanted an app on my phone that saves a spot in seconds, lets me add the details later, works offline and keeps my spots to myself.
On the phone · Expo Go
Needs a signal
Capture now, details later
The Method
Diagnose
In February I wrote the product spec, and four interactive sprints built every screen but the map; then it stalled: nothing had ever run on a phone. When I picked it up again in October, the first job was getting it onto my iPhone, and Expo Go runs only the newest SDK.
Decide
The overnight spec fixed the big calls: upgrade the SDK one version at a time inside a 2-hour box, Apple Maps over Mapbox, and photos copied into the app, never written to the camera roll. When the agent put batch import on the Profile tab, I moved it to the Spots tab.
Build
One overnight agent loop took it from compiling to ready for my phone: SDK 54 to 57, the first green CI, real SQLite migrations, photos in app storage, Apple Maps and named untitled spots. A second session ran as an orchestrator: Sonnet wrote the failing tests, Opus implemented and Opus reviewed. Tests went from 115 to 220 to 374.
Verify
Every feature and fix started as a failing test, and from M3 on the database code ran against real SQLite, not mocks. Every milestone got an independent review, and every one that touched a screen ended with a table of what the screen would show, row by row. Then I used it on my iPhone, which found what 374 tests could not.
Simplify
After using it, I wrote a spec that cuts a 20+ field form in five sections to seven fields: name, location, photos, feature types, skate stopped, natural speed and notes. In it, stars become a five-step speed scale, gear and drafts go, and the old details are archived, not deleted. This is specified, not yet built.
Key Decisions
- D1
One SDK version at a time
Chose upgrading Expo from SDK 54 to 57 one version per step inside a 2-hour box, falling back to the last green step, over a single jump to 57 because Expo Go on my phone runs only the newest SDK, and a failed step should cost one hop, not the night.
Cost: Each hop broke something different, from a nested module to a router change and a dependency conflict, so each was fixed and tested on its own before any feature work.
- D2
Apple Maps now, Mapbox later
Chose Apple Maps through react-native-maps over Mapbox because Apple Maps needs no API key and no custom build, so it runs in Expo Go today.
Cost: No offline map tiles until a custom build makes Mapbox possible.
- D3
Photos stay inside the app
Chose copying every photo into the app's own storage and saving only its relative path, over keeping the camera's cache file, which iOS can purge, because a photo survives iOS clearing its cache, my camera roll stays untouched, and deleting a spot never takes a photo with it.
Cost: Code never deletes a photo file, so a deleted spot's photos stay on the phone.
- D12
Import lives on the Spots tab
Chose a second button on the Spots tab, beside the + for a new spot, over the Profile tab, where the agent first put it, because new spots start on the Spots tab, in List and Map, and an import button on Profile is bad UX.
Cost: Found late: until the phone guide was written, batch import had no way in at all.
- D21
Natural speed in words, not stars
Chose a labelled five-step scale (None · Little · Some · Enough · Too much) over a row of five stars because stars read as "how good", and five identical stars say nothing about what is being measured.
Cost: Specified, not yet built. Old star ratings move down one step to fit the 0–4 scale.
- D22
Gear removed entirely
Chose removing gear from the app over a "gear I have" filter because filtering by what I own was the wrong way round, a spot that takes either of two setups makes any gear list ambiguous, and what gear really meant was "needs extra speed", which the speed scale already says.
Cost: Specified, not yet built. Old setup options are archived, not deleted.
How It Evolved
Feb 12–14 · Spec and sprints
I wrote the product spec, and four interactive sprints built every screen but the map.
The loop never closed on a device, and the project stalled for eight months.
Oct 13 · The revival
A planning session read the repo as it stood and wrote the overnight spec: the decisions, the milestones and a 2-hour box for the SDK upgrade.
I wanted it on my phone, and that meant three SDK versions first.
Oct 14–15 · Overnight loop
One agent loop worked milestones M0 to M7: SDK 57, the first green CI, real SQLite tests, photos, the map, named untitled spots and a reachable import. Tests went from 115 to 220.
It ran end to end in tests and bundled for iOS; clustering, search and the edit screen were next.
Oct 15 · Orchestrator session
A second session split the work: Sonnet wrote the failing tests, Opus implemented, Opus reviewed. Clustering, sort and search, edit fixes and foreign keys landed: 374 tests in 37 suites, CI green.
Every gate was green. The phone had not had its turn yet.
Oct 15 · On the phone
Importing several photos crashed on my iPhone, a failure no test could see. Ids moved to expo-crypto, and a lint rule now blocks the old import.
Every test had passed; the phone found what they could not.
Oct 16 · Simplification spec
After using it, my feedback became a spec: from 20+ fields to seven, a five-step speed scale instead of stars, no drafts and no gear, with the old details archived, not deleted.
It is specified, not yet built: the next agent run builds it from that spec.
Before · 20+ fields in five sections
After · 7 fields, specified Oct 16
- Nameoptional
- Locationthe one field a spot needs
- Photosa grid; add or remove
- Feature typesrail, ledge, bank …, pick any
- Skate stoppedone switch
- Natural speedEnough: rolls in with speed for most tricks.NoneLittleSomeEnoughToo much
- Notesone free-text field
What Broke
What broke All 374 tests passed, and importing several photos crashed on my iPhone: the uuid package needs crypto.getRandomValues(), which Node and Jest have and the phone's Hermes engine does not.
The rule The test environment is not the device. Ids now come from expo-crypto, and a lint rule blocks the uuid import.
What broke The test database enforced foreign keys and the phone's did not: node:sqlite turns them on by default, expo-sqlite starts with them off. Review then found that a future table rebuild with them on would have deleted every photo record.
The rule Test fakes start the way the device starts. Foreign keys are off during migrations and on afterwards.
What broke A spot saved without GPS showed as "7679.1 mi" away in the list. No test failed; a table of what the screen would show, row by row, exposed it.
The rule A spot with no location shows no distance. The table stayed: every milestone that touches a screen ends with one, on this site's build too.
What broke A row of sort chips inherited React Native's default flexGrow: 1 and would have filled about half the Spots screen. Jest can't see layout; a reviewer caught it by reading the React Native source.
The rule Passing tests say nothing about layout. Layout gets checked against the framework's source and on the phone.
What broke Saving a spot in Edit silently wiped its skate-stop details, rounded untouched dimensions on every save, and stored "abc" as NaN.
The rule A save keeps every field it did not touch. A failing test reproduces the loss before the fix.
Outcome
When the second session wrapped up on the evening of Oct 15, the app worked end to end in tests and bundled for iOS: Quick Add, batch import grouped by place, the list and the Apple Maps view, search and sort, all offline-first. The suite stood at 374 tests in 37 suites with CI green, expo-doctor at 21/21 and a 4.1 MB iOS bundle. Then, on my iPhone in Expo Go, importing several photos crashed. Once that was fixed I used it, and on Oct 16 my feedback became a spec for the cut to seven fields. As of Oct 18 that simplification is specified, not yet built. The app still loads in Expo Go from my desktop; a TestFlight build comes later.
How it was builtI wrote the product spec, made or answered the calls on this page (keeping notes, the speed-scale words and the archive are defaults I can still overrule), and tested the app on my phone. Claude Code agents wrote much of the code under that direction: one overnight loop, then a session where Sonnet wrote the failing tests, Opus implemented and Opus reviewed.