DevelopmentMobileBackendIntegrations
04
Flutter Starter Kit

Relocate

Flutter foundation for a US neighborhood-research app — Mapbox, Firebase, RevenueCat paywall, Sentry + Crashlytics, delivered as a collaborator on a client-owned product.

Introduction

Relocate is a cross-platform mobile app that helps people researching a US neighborhood see what living there actually looks like — census demographics, school ratings, crime patterns, flood risk, weather, cost of living, and resident stories on one map. My role was to build the Phase 1 Flutter starter kit: app shell, navigation, design system, subscription flow, onboarding, Mapbox integration, crash/observability pipeline. The product direction, business model, and post-handoff development are owned by the client, who continues development on their side. I was a collaborator on this engagement, not the sole builder or owner — the brief was a production-ready foundation the client team could hand off and keep building on.

The Challenge

Mapbox N+1 platform-channel calls. The school, offender, and nearby screens each rendered hundreds of map annotations one at a time, each create() round-tripping through a platform channel — visibly slow on mid-range Android. Switched to createMulti(options) so the entire batch crosses the channel once.

RevenueCat entitlement Unicode trap. The original entitlement ID was Relocate․app Pro — that dot is a U+2024 ONE DOT LEADER, not an ASCII period, copy-pasted from a designed mockup. Visually equal, codepoint-unequal, so the SDK returned active: false in production. Renamed to plain ASCII pro and documented the rename for the RevenueCat dashboard.

Trial paywall CTA gated on real SDK data. The CTA was previously hardcoded — users in regions without an introductory offer saw a "Start free trial" button that resolved to a normal purchase. Now reads package.storeProduct.introductoryPrice directly; CTA only shows trial wording when the SDK surfaces one.

Sentry filter for VM/farm SIGABRTs. Firebase Test Lab's Robo crawler and other emulators were flooding Sentry with native crashes that never affect real users. A beforeSend filter fingerprints four signals — Google buildbot kernel banner, sub-phone screen geometry, malformed QKR1.* build IDs, <=2 processor count — and drops events that match any. Real-device events pass through unchanged.

iOS reCAPTCHA fallback for phone auth. Firebase Phone Auth on iOS falls back to a reCAPTCHA web flow when APNs silent push isn't available (simulators, devices missing entitlements). Wired the REVERSED_CLIENT_ID URL scheme into Info.plist and matched the OAuth client.

Landscape layout pass. Full responsive sweep across all 27 screens. Drawer compaction at height < 520, vertical centering of nav items in compact mode, full-bleed background under iPhone Dynamic Island (override colorScheme.surface to black in landscape + dark mode), content-width caps for prose vs lists vs forms via four typed constraint tokens (1200 / 600 / 480 / 400 dp).

Mapbox token init race condition. setAccessToken() returns void but writes to a pigeon channel asynchronously. Without an await before runApp, MapWidget could mount before the token landed on the native iOS SDK — blank tiles on cold start. Bounded await with a 500ms timeout fallback fixes the warm-sim race without risking deadlock on the cold-boot pigeon handshake.

Our Solution

Flutter 3.38+ with Riverpod 3 and GoRouter 17. Feature-first layout under lib/features/auth, neighborhood, neighborhood_stories, onboarding, search, checklist, commute — with data-heavy features split into presentation/, domain/, data/ layers. Shared code in lib/core/. Riverpod 3 with code generation drives state and async data; ref.invalidate after every write keeps caches honest. Freezed models guarantee immutability across async boundaries.

Mapbox integration with batched annotations. Point and circle annotation managers, batched annotation creation via createMulti(), geocoding-driven address search, light/dark style swap that follows the app theme, and defensive disposal to absorb mid-RPC platform-view teardowns.

RevenueCat paywall with trial-aware CTA. Hard paywall gate route (/paywall) for non-Pro users. Custom entitlement claim verification. Trial CTA reads introductoryPrice from the SDK directly — no hardcoded strings. Restore purchases flow included.

Dual crash reporting: Sentry + Crashlytics. Sentry initialized first inside its own appRunner zone, then Crashlytics layered on top by chaining — never replacing — FlutterError.onError and PlatformDispatcher.onError. Firebase Auth UID pushed to both as the user identifier on every auth state change. VM/farm SIGABRT filter in beforeSend keeps the noise floor low.

RelocateNeighborhoodLayout scaffold. AppBar, hamburger drawer, glassmorphic 10-tab NeighborhoodTabBar — keeps all neighborhood sub-screens visually consistent without per-screen Scaffold boilerplate.

Two complete themes. "Informed Curator" light theme (Manrope + Inter, surface hierarchy via tonal shifts, no 1px borders) and a tactical/mission dark theme (JetBrains Mono headlines, cyan #00F2FF primary). Both bundled as font assets.

Features delivered. 4-slide cinematic onboarding carousel ending in a paywall gate. Firebase Phone Auth with country picker, SMS OTP, and debug-build appVerificationDisabledForTesting. Moving checklist, favorites, profile + edit-profile, settings, briefing, stories (feed + detail + create), cost-of-living, weather, flood, offender registry, commute calculator. iOS + Android release builds with obfuscation, split debug info, Sentry symbol upload, and Crashlytics native symbol upload via the Gradle plugin.

Outcome

The Phase 1 starter kit was handed off to the client, who owns the product and continues development on their side. The codebase runs to 38,711 lines across lib/ (excluding generated files), 30 Riverpod providers, 40 Freezed models, 45 tests, and 625 commits authored.

The client confirmed they're happy for this to be featured as a case study, framed accurately as a prototype and starter kit — since significant work remains before a public release. Available for new client engagements with similar scope — Flutter app foundations, subscription flows, and observability pipelines.

Conclusion

Relocate is a collaborator engagement, not a solo product — and that distinction matters. The brief was to deliver a production-ready Flutter foundation: clean architecture, working paywall, real Mapbox integration, dual crash reporting. That's what shipped.

The interesting technical work was in the details: a Unicode codepoint hiding in an entitlement ID, a batch API call that cut hundreds of platform-channel round-trips to one, a beforeSend filter that keeps emulator noise out of production alerts. The foundation is solid. The client builds from here.

More live work

More live work

Want one of these?

Built Relocate end-to-end. Yours next.

One engineer, full stack, shipped fast. No PM layer, no handoffs.