BACK_TO_BLOG
·3 min read·Building / AI / Engineering

Ride-test week: fixing the planner and the group map

A heavy week on Switchback — the AI route planner got a real conversation loop, the group-ride map hit nav parity, and iOS IAP shipped.

Eighty-three commits on kickstand-web this week. Almost all of it came out of actual ride-testing — the fastest way I've found to figure out which parts of an app are wrong.

The planner got conversational

The biggest piece was #336: the one-shot AI route builder is now a clarify-then-build-then-refine chat with an interactive map. Ask for "best route to X" and it doesn't silently inject scenic detours anymore — plain "best/fastest" now defaults to curviness 0 (#338, #379), and scenic atlas roads are opt-in with per-road add/remove chips (#341). Riders pick the scan radius themselves — 20/50/100/200 mi (#348).

The AI's full "why" now lives inline as the chat bubble on both web and mobile (#337), instead of a disconnected block below the composer. And surface + grade come back on every route from ORS extra_info, with a Pro "avoid unpaved" toggle plumbed through a clean Mapbox engine seam (#375, #380).

The pattern here is the one I keep coming back to on AI features: the model isn't the product. The seams around it are. A conversational planner is only useful when the intent parsing, the geocode disambiguation, the scenic-bias controls, and the map editing are all sharp enough that the LLM isn't papering over UX gaps.

Group ride map, nav parity

The live group map got closer to a real navigation surface. Full-screen follow-me by default, recenter/follow toggle, yellow turn card, weather + fuel chips grouped at the top (#359, #362, #365). Hazard reporting from the map (#355). Tap a puck to see rider name / bike / distance (#354). Cardo-style one-tap group messages — "I need fuel", "Watch out" — over the existing realtime channel, no new table (#377).

iOS IAP and a security cleanup

RevenueCat / StoreKit landed for App Store 3.1.1 compliance (#320), with a per-source tier column model (polar_tier, apple_tier, comp_tier) merged into profiles.tier, highest wins. Along the way I found and closed two related holes:

  • A broad RLS policy let any authenticated user self-set profiles.tier = 'founder' (#319).
  • The column-level REVOKE I initially shipped was ineffective — Supabase's table-level UPDATE grant overrides it — so I moved to a trigger guard (#321).
  • The /revenuecat/sync backstop was trusting client-supplied entitlementIds and writing them with the service role. Any signed-in rider could POST {"entitlementIds":["elite"]} and grant themselves Elite. Now it verifies with RevenueCat first (#352).

Three variants of the same class of bug — trusting the client on entitlement writes — found within a few days of each other. That's how these things go: you find one, you know there are more.

Small stuff that adds up

Reroute now waits 5s of continuous off-route instead of a 2s hit-counter, so a brief dip onto a scenic road doesn't snap you back (#372). Speed readout got a rolling average so the big number stops twitching (#374). Voice ducking releases reliably again on iOS (#373) — expo-speech's onDone doesn't always fire, and prompts every 8s were stacking live utterances and pinning the audio session active. Riders had to lock the screen to get their music back.

None of these are glamorous. They're the reason the app feels like it works.

Back at it Monday.