Back to Blog

Building Galazio: Mapping Greek beaches with live water quality

Emmanouil Athanasopoulos6 min read1,188 words
React NativeMapsAPI IntegrationMobile

The Motivation Behind the Project

Galazio was built to make exploring the water quality of Greek beaches easier and more visually engaging. Greece has over 1,600 officially monitored bathing water sites, and the data is publicly available through the European Environment Agency, but it is buried in spreadsheet-style tables that are difficult to browse on a phone. I wanted to create an app that took that data and presented it on an interactive map where users could tap a beach, see its quality rating at a glance, browse real photos, and access the full 10-year quality history. The main technical challenge was rendering 1,600+ interactive markers on a mobile map without sacrificing performance — a problem that turned out to be much harder than I initially expected.

Core Features and Design Goals

  • Live EEA Data Integration & Offline Cache: the app fetches real-time water quality classifications directly from official European databases. Data is cached locally via AsyncStorage, allowing users to browse beaches even without cellular reception on remote islands.
  • High-performance MapLibre Bridge: instead of standard native maps, the app uses a custom WebView bridge to run MapLibre GL JS for buttery smooth 60fps interaction. Smart DOM filtering ensures only markers within 25km are rendered, avoiding heavy layout thrashing.
  • Global Image Caching with Supabase: dynamically loads tourist photos via Google Places API. To save API costs, images are permanently cached globally in a Supabase backend the first time they are resolved, ensuring future users load them instantly for free.
  • Live Community Condition Reports: users can crowdsource live conditions directly from the beach (e.g., Jellyfish, Seaweed, Crowds, Waves). Reports are broadcast globally in real-time using Supabase Websocket subscriptions.
  • Hyper-Local Weather & Wind: integrates the Open-Meteo API to display exact temperature, wind speed, and a dynamically rotating wind compass needle based on the specific coordinates of the beach.
  • Intelligent Blue Flag Algorithm: a heuristic algorithm analyzes the 10-year historical EEA water quality data, dynamically awarding prestigious 'Blue Flag' badges to beaches maintaining 'Excellent' ratings for 5 consecutive years.

Deep Dive: How It Works Under the Hood

The core technical story of Galazio is about solving a performance problem that does not have an obvious solution in React Native. When I first prototyped the map with react-native-maps and standard Marker components, the app was barely usable — rendering 1,600 custom SVG markers caused the Android UI thread to stall for several seconds on initial load, and panning the map was choppy at best. I tried marker clustering, but the beach data is geographically dense along the Greek coastline, and clusters would not resolve into individual markers until unreasonably high zoom levels. The solution I landed on was building a custom WebView bridge to MapLibre GL JS. Instead of rendering native markers, the app runs a full MapLibre GL JS instance inside a WebView, and I communicate between React Native and the WebView using injectJavaScript (RN → WebView) and window.ReactNativeWebView.postMessage (WebView → RN). The map renders using CARTO dark-matter basemaps for a clean aesthetic, and markers are injected as HTML elements with CSS transforms. The critical optimisation is the bounding box filter: every time the user pans or zooms (the map's moveend event), the WebView calculates which beaches fall within a 25km radius of the viewport center using the Haversine formula, and only those markers are present in the DOM. This keeps the active marker count under 200 at any time, which is well within what a mobile WebView can handle smoothly.

Technical Implementation

  • Built a custom WebView map bridge using MapLibre GL JS, communicating bidirectionally with the React Native layer via `injectJavaScript` and `window.ReactNativeWebView.postMessage`. This bypassed the severe performance limitations of standard react-native-maps when handling highly customized SVG markers.
  • Engineered a Supabase Global Image Cache architecture. When a user queries a beach, the app checks Supabase for an existing photo URL. If missing, it fetches the raw Google Places redirect URL, persists it to Supabase, and serves it to all subsequent users—cutting API costs drastically.
  • Implemented a Real-time Crowdsourcing Widget using Supabase `postgres_changes` channels. Users can submit condition reports which are immediately broadcast to all active clients viewing that beach, coupled with optimistic UI updates and haptic feedback.
  • Designed a robust Offline Fallback Strategy using `AsyncStorage`. The EEA dataset is fetched once and cached locally, allowing the app to seamlessly switch to offline mode if the user loses network connectivity.

Architecture Overview

The app follows a three-layer architecture. The data layer handles all API communication (EEA ArcGIS, Google Places) and caches responses in memory for the session. The bridge layer manages the WebView-to-RN communication protocol, serialising marker data as JSON and deserialising tap events. The UI layer handles the detail sheets, search, and navigation. These layers are deliberately decoupled so that swapping the map engine (for example, replacing MapLibre with Mapbox) would only require changes in the bridge layer.

Challenges I Faced Along the Way

The biggest challenge was not the map rendering itself — it was the data pipeline. The EEA ArcGIS REST API returns water quality data in a format that requires significant transformation before it is usable. Beach names are inconsistent (sometimes in Greek, sometimes transliterated, sometimes abbreviated), coordinates can be imprecise, and the quality classifications use coded values that need to be mapped to human-readable labels. I built a normalisation layer that cleans up all of this at fetch time, but it required weeks of testing against the full dataset to handle edge cases. Another challenge was the WebView communication latency. Injecting JavaScript into a WebView is asynchronous and can take 10-50ms per call on lower-end devices. I had to batch marker updates and debounce pan events to avoid flooding the bridge with messages.

The Technology Stack

Built with Expo Router, React Native, and TypeScript. The mapping core relies on a carefully tuned WebView running MapLibre GL JS with CARTO dark-matter basemaps. Backend logic and real-time syncing are powered by Supabase. Data is sourced from the EEA ArcGIS API, Google Places API, and Open-Meteo API. The UI extensively utilizes Lucide Icons, Reanimated, and AsyncStorage.

What I Would Do Differently Next Time

If I were to rebuild Galazio, I would evaluate using MapLibre Native (the native iOS/Android SDK) instead of the WebView bridge approach. The WebView solution works well, but it adds a layer of indirection that makes debugging harder — when something goes wrong in the map, I have to inspect the WebView's JavaScript console separately from the React Native debugger. I would also invest in pre-processing the EEA data on a lightweight server instead of doing all the normalisation on-device, which would reduce the initial load time significantly.

Final Reflections

This project highlights how sometimes the best way to handle heavy mobile mapping requirements is to build a smart bridge directly to a robust web engine. It also taught me that performance optimisation is not always about writing faster code — sometimes it is about being smarter about what code you choose not to run. The bounding box filter, which simply removes markers from the DOM when they are out of view, had a bigger impact on performance than any amount of rendering optimisation could have achieved.