Almost all the week's commits landed in kickstand-web — the Switchback Moto monorepo. 97 PRs is more than I usually merge, and most of them were not maintenance. Some of what actually shipped:
Billing got real
The pricing model went from "Free / Premium" handwaving to four honest tiers: Free, Pro, Elite, Founder. The entitlement layer (#76) gates saved-route limits, maintenance log, leaderboards, group rides, and ride buddies. Free is now capped at 5 saved routes. Pro and Elite unlock the rest. Admins are unmetered.
The Polar integration finally moved sandbox → production (#79 captures the checklist — webhook events, token scopes, the Vercel "Value field" gotcha that made our webhook return 503 instead of 403). #66 was the actual root cause: turbo.json strict-env was silently stripping POLAR_*, ORS_API_KEY, CRON_SECRET, and the Sentry DSN out of the deployed app. Declared, fixed, shipped. Plan & billing page now shows "Unlimited" for admins instead of leaking the 1,000,000 sentinel (#69).
Monthly/annual toggle on the plan page (#78) — the upgrade and change actions had been hardcoded to annual. Embarrassing once spotted.
Badge math, honestly
The profile trophy case was iterating only the catalog, so riders who earned a Limited Time badge (Founder, Launch Rally, Beta Pioneer) saw the celebration but never the cell (#80). Same bug a layer down for route + rally badges (#92). #68 added a tested helper that computes a rider's collection completion across all three families — the numerator is real rider_badges rows, the denominator is the real available total, no fake percentages.
#91 was the one I liked most: iconic roads on a profile no longer come from a self-select toggle. A road shows "ridden" only when the rider earned that road's route badge from a real route_runs row. Free toggles were dishonest. Now it's evidence-based or it doesn't render.
Mobile reached parity
The mobile app picked up the AI route builder on the Plan screen, a real map (react-native-maps behind the existing RouteMap seam — same prop contract web uses), glove-first hazard reporting on Live Nav (round 62px button, 2×3 grid of ≥84px tap targets), the trophy case, real follow counts and lifetime ride stats, Plan → Live Nav handoff, and an AuthGate that actually requires sign-in (#85) instead of the first-launch-only onboarding it had before.
The planner AI endpoint learned to accept a Supabase bearer token from mobile clients (#31), validated server-side via auth.getUser(jwt). Fails closed on bad or expired tokens. No cookie fallback for the mobile path.
Blog automation
Phase 1 of a state-spotlight generator (#57) — researches a state, links existing atlas POIs, and discovers verified roads, stops, and rallies not yet in the atlas so they can be drafted as inputs to the POI pipeline. Drafts only. No auto-publish, no DB writes. A pnpm blog:next-state picker (#86) walks regions alphabetically and prints the next one needing a spotlight, so the twice-weekly GitHub Action has something deterministic to run. Alabama was the first one out the door (#89). Caught a Node 20 WebSocket polyfill bug along the way (#87).
Honest fixes
Sentry was reporting every error from local next dev because .env.local set NEXT_PUBLIC_ENV=production for the prod Supabase stack. The 14-day backlog all traced back to localhost. Gated Sentry.init on NODE_ENV=production (#38) and the backlog evaporated.
Big week. Next one will be smaller.