❯ AI · Mobile · 2025
Nearby
A Flutter + Serverpod app that pings AI-generated facts about landmarks via background proximity detection — 237 mobile + 191 API tests green.
The problem
Tourists and curious locals pass a hundred landmarks a day and learn nothing about them. A notification that explains the building you are standing in front of turns a walk into a discovery — if it can know where you are without draining your battery. The whole product lives or dies on that constraint.
The approach
A Flutter + Serverpod monorepo app (iOS + Android) pings AI-generated facts about landmarks via background proximity detection. The design constraint is cost and battery: continuous geofencing would burn both. So the system works on a fixed ~1.1km grid with on-demand coverage backfill — a cell is populated the first time it is needed, memoized for 30 days, and capped per device per day.
Facts come from Gemini via Vertex AI with fail-soft behavior, gated by a server-enforced 50/month free-tier cap. Auth uses Application Default Credentials, so there are no API keys on devices to extract or rotate.
Architecture
- Flutter App
- Core API (Serverpod)
- Geofence Ingestion
- Landmark Matching
- Fact Generation (Gemini)
- Billing / Entitlement
- Notification Dispatch
FLOWS
- Flutter Appbackground locationGeofence Ingestion
- Geofence Ingestioncell matchLandmark Matching
- Flutter AppREST APICore API (Serverpod)
- Core API (Serverpod)~10-fact requestFact Generation (Gemini)
- Fact Generation (Gemini)FCM/APNsNotification Dispatch
- Flutter AppverifyBilling / Entitlement
Background proximity runs on flutter_background_geolocation plus geolocator. When the device crosses into a grid cell, the app matches against known landmarks in PostGIS and backfills coverage on demand; the 30-day memo and per-device daily caps keep both cost and battery bounded.
The client-server split is service-oriented — Geofence Ingestion, Landmark Matching, Fact Generation, Notification Dispatch, Core API, Map Service, Billing/Entitlement — on Serverpod with PostgreSQL + PostGIS, Redis, and FCM + APNs push. Fact generation is fail-soft: when Gemini is slow or down, the app degrades gracefully instead of failing the notification path.
Entitlement is server-authoritative: the server verifies Apple/Google purchases and fails closed when the app store is unreachable, so there is no client-side entitlement to spoof and no accidental free upgrade. Sixteen features and ten screens include a motion-rich fact-reveal card in Rive, favorites, discovery history, a clustered map view, and detection-radius settings. The design-token system maps into native Flutter ThemeData with a single motion driver, reduced-motion fallbacks, and WCAG AA accessibility with 48px touch targets. The suite runs 237 mobile and 191 API tests green.
Key decisions
Background proximity with grid coverage
A fixed ~1.1km grid with 30-day memo beats continuous geofencing for both cost and battery.
Server-authoritative entitlements that fail closed
If the app store is unreachable, nobody gets a free upgrade by accident.
Fail-soft fact generation
The notification is the product; a Gemini hiccup should degrade gracefully, not take the app down.
Test volume as a feature
237 mobile + 191 API tests are the reason the app can add a feature without breaking the background path.
Design tokens as the contract
Tokens mapped into native ThemeData keep the UI and the motion driver reading from one source, and make reduced motion a first-class path instead of a retrofit.
What I'd do differently
The coverage backfill initially ran on a cron that outgrew its slot; moving it to an on-demand model earlier would have cut the first latency complaints. I would also instrument usage before fixing a free-tier number — 50/month was the right order of magnitude, but it was set before there was data to justify it.