Skip to content
Projects

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

React NativeExpoTypeScriptexpo-routerzustandSQLitereact-native-mapsJestGitHub Actions
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

Screens Spots (List · Map) · Quick Add · Import · spot detail and edit
zustand stores spots with filters, search and sort · media · settings · location
SQLite offline-first; versioned migrations
Photo files photos copied into app storage; never written to the camera roll

Needs a signal

Apple Maps react-native-maps; no API key, no custom build
Apple's geocoder a town name for untitled spots; older ones backfilled at most 10 per launch

Capture now, details later

Capture the camera, or photos from the library grouped by place
Review a name is optional; the location comes along
Details later saved as a draft; the full form whenever I get to it
Spots, photos and the database live on the phone; only the map and the town names need a signal.

The Method

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

Feature type, custom type, shape, length · height · width, material, skate stops, difficulty
In-Run natural speed in 1–5 stars, setup options for gear and build
Landing difficulty, surface
Details intensity, bustability, bust notes
Status draft or complete

After · 7 fields, specified Oct 16

  1. Nameoptional
  2. Locationthe one field a spot needs
  3. Photosa grid; add or remove
  4. Feature typesrail, ledge, bank …, pick any
  5. Skate stoppedone switch
  6. Natural speed
    None
    Little
    Some
    Enough
    Too much
    Enough: rolls in with speed for most tricks.
  7. Notesone free-text field
Specified on Oct 16 after using the app, not yet built: a spot is a place with photos, and everything else is optional.

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.