BACK_TO_BLOG
·3 min read·Building / Engineering

Shipping 1.0, then fixing what 1.0 broke

Switchback went live on the App Store this week. Then the real bugs showed up — auth hangs, silent ride-save failures, and a badge celebration that never fired.

Switchback shipped to the App Store. The marketing site flipped from "coming soon" to a download button (#484 — homepage hero now leads with NEXT_PUBLIC_APP_STORE_URL when set, with a graceful waitlist fallback), and I sent the "live on iPhone" launch email through a Resend script with dry-run, --test, and --send modes plus per-recipient HMAC-signed unsubscribe (#490).

Then the real bugs showed up.

The stale-token cascade

The core failure was subtle: a ride runs long with the app backgrounded, RN's Supabase auto-refresh timer gets suspended by iOS, and the access token comes back expired. auth.uid() returns null, ride save fails, badges don't award, and the summary screen shows [object Object] Done because a plain PostgrestError isn't a JS Error.

That one root cause fanned out into most of the week:

  • #468 moved road-badge awarding off the fragile client call and into AFTER INSERT triggers on free_rides and route_runs. If you rode the road, you get the badge — even if the token expired between the finish tap and the save.
  • #470 fixed the summary screen to actually reflect what saved.
  • #508 added processLock to Supabase auth so RN (which has no Web Locks API) can serialize concurrent auth calls instead of hanging forever on a stalled refresh.
  • #510 traced a real signOut() hang to clearPushToken() calling supabase.auth.getUser() — a network call with no timeout — as its first line. processLock was a red herring for that one.

Crash reporting, finally

The OTA crash a few weeks back couldn't be symbolicated without a Mac, which was the moment I knew we had no real production visibility. #498 added @sentry/react-native, #499 fixed the pnpm hoisting issue that broke the source-map upload build phase, #502 fixed the EAS env-var config that was silently disabling Sentry in production, and #505 added a captureError() helper that wraps non-Error throws so Supabase's PostgrestError shows up in Sentry with a real title and stack instead of Object captured as exception with keys: code, details, hint, message.

The first real Sentry issue came in almost immediately: #506 and #507award_track_road_badges was throwing 42702 "ambiguous column reference" because the function's RETURNS TABLE (badge_id, road_name, slug) makes those OUT variables in scope, and the CTE was selecting unqualified slug. Post-ride badge celebration had been silently failing for who knows how long. That is exactly why you buy the crash reporter before you need it, not after.

The version-train tax

Two things I did not know before this week: once Apple approves 1.0.0, the "1.0.0 train" closes and every subsequent build must exceed it (ITMS-90186 / ITMS-90062). I learned this by getting rejected on build 17 under 1.0.1 after 1.0.1 went live, which cost a ~20-minute build-submit-reject cycle. #501 added a version:check script that hits the public iTunes lookup and fails the build if the version needs to bump. Cheap fix, saves the same 20 minutes every time from now on.

Small things

Also shipped: an in-app "Rate Switchback" prompt via App Store deep link, since expo-store-review isn't in this build (#482); an OTA CI workflow that publishes from Linux so the fingerprint matches the EAS-built binary (#488); and a segment-trim feature for scenic byways that follow only part of a numbered highway (#476) — used to publish another dozen roads across Kansas, Missouri, Iowa, and the Ozarks.

Launch week is not the finish line. It's the week the assumptions get tested.