Architecture
weNow is a multi-source weather aggregator. The React Native app communicates with a Cloudflare Workers backend, which fans out requests to multiple weather APIs in parallel, normalizes responses into a shared data format, computes a weighted ensemble median, and returns the results to the client.
System Overview
The system consists of three tiers: a cross-platform mobile app, a serverless backend, and the upstream weather APIs that supply raw data.
+--------------------------+ +-------------------------------+
| React Native App |------>| Cloudflare Workers Backend |
| (Expo SDK 54) | | (wenow-api) |
| | | |
| - WeatherScreen | | POST /api/weather |
| - SettingsScreen | | POST /api/weather/widget |
| - AccuracyScreen | | GET /api/weather/status |
| - LocationPickerScreen | | GET /api/geocode |
| - PaywallScreen | | GET /api/geocode/reverse |
| - Android Widgets (4) | | GET /api/accuracy |
| - AdBanner | | GET /api/ads |
| - 50 languages | | POST /api/ads/click |
+--------------------------+ | POST /api/feedback |
| POST /api/auth/register |
| POST /api/auth/login |
| GET /api/auth/me |
| POST /api/users |
| POST /api/webhooks/revenuecat|
+---------------+---------------+
|
+-----------+-----------+-----------+
| | | |
v v v v
Open-Meteo OpenWeather WeatherAPI Visual
(ECMWF) Map (GFS) (blend) Crossing
(blend)
| Component | Technology | Role |
|---|---|---|
| Mobile App | React Native + Expo SDK 54 | UI, city search, location, widget rendering |
| Backend | Cloudflare Workers + D1 (SQLite) | API proxy, rate limiting, ensemble computation, auth, feedback |
| Open-Meteo | ECMWF model | Highest-weighted source (weight 4) |
| OpenWeatherMap | GFS + proprietary | Second-highest weight (weight 3) |
| WeatherAPI | Multi-model blend | Blended source (weight 2) |
| Visual Crossing | Multi-model blend | Blended source (weight 2) |
Three additional sources (AccuWeather, WeatherBit, Tomorrow.io) are implemented but currently disabled pending API key fixes. Their adapter code and ensemble weights are ready for re-activation.
Data Flow
Every weather request follows the same pipeline, whether initiated by the main app or an Android home screen widget.
-
User searches a city (or taps "Use My Location" for GPS reverse-geocode).
City autocomplete calls
GET /api/geocode?q=...with 300ms debounce. -
App calls the backend via
POST /api/weatherwith{lat, lon, name, lang}. Widget requests use the dedicatedPOST /api/weather/widgetendpoint instead. -
Backend fans out to all active weather APIs in parallel using
Promise.allSettled. Each API adapter normalizes its response to the sharedWeatherDatainterface (all temperatures in Celsius, wind in m/s). -
Ensemble computation: the backend runs
computeEnsemble(data), which applies source weights (ECMWF > GFS > blended), removes statistical outliers beyond 1.5 standard deviations, and calculates a weighted median for temperature, humidity, pressure, wind speed, and multi-day forecasts. -
Backend responds with
{data[], ensemble, errors[], skipped[], rateLimit}.datacontains per-source results;ensemblecontains the aggregated median with confidence metadata;errorslists any sources that failed;skippedlists sources that hit their global API quota. -
App renders the ensemble summary at the top, then per-source
WeatherCardcomponents, followed by a 7-dayForecastCard. - Location is persisted to AsyncStorage so Android widgets can read it on their next update cycle.
Key Patterns
Partial Failure Tolerance
All API calls are dispatched with Promise.allSettled, which never rejects.
If one or more sources fail or time out, the app continues to function with the
remaining sources. Failures are collected in the errors[] array so the
client can display a notice without breaking the UI.
Unit Normalization
Every API adapter converts its raw response to a common set of units before returning
a WeatherData object:
- Temperature: Celsius
- Wind speed: m/s
- Pressure: hPa
- Precipitation chance: 0–100 (percentage)
Weighted Ensemble Aggregation
The ensemble algorithm in backend/src/ensemble.ts assigns static weights
based on forecast model quality:
| Source | Underlying Model | Weight |
|---|---|---|
| Open-Meteo | ECMWF | 4 |
| OpenWeatherMap | GFS + proprietary | 3 |
| Visual Crossing | Multi-model blend | 2 |
| WeatherAPI | Multi-model blend | 2 |
Outliers beyond 1.5 standard deviations from the mean are removed before computing the weighted median. When sources agree within 1.5 degrees Celsius, confidence is rated high; within 3 degrees, medium; beyond 3 degrees, low.
Rate Limiting
Per-IP rate limiting is enforced at three separate tiers, with IP addresses hashed using SHA-256 for GDPR compliance (no raw IPs are stored):
| Endpoint Category | Daily Limit per IP |
|---|---|
App weather (/api/weather) | 5 requests/day (currently elevated for development) |
Widget weather (/api/weather/widget) | 100 requests/day |
Geocoding (/api/geocode, /api/geocode/reverse) | 200 requests/day (shared) |
Ads fetch (/api/ads) | 200 requests/day (click endpoint unrestricted) |
Feedback (/api/feedback) | 5 requests/day |
Auth register (/api/auth/register) | 10 requests/day |
Auth login (/api/auth/login) | 30 requests/day |
Accuracy (/api/accuracy) | 20 requests/day |
User registration (/api/users) | 10 requests/day |
Debounced City Search
The CityAutocomplete component debounces keystrokes at 300ms before
calling the geocode endpoint, preventing excessive API calls during typing.
Local State Only
All UI state lives in React useState hooks. There is no global state
management library (no Redux, MobX, or Zustand). This keeps the codebase simple and
avoids synchronization complexity. Persistent data (selected city, language preference)
is stored in AsyncStorage.
ErrorBoundary
An ErrorBoundary component wraps the entire app, catching unhandled
component crashes and rendering a fallback screen instead of a white screen or
hard crash.
Source Layout
App (src/)
src/
components/
WeatherCard.tsx -- Per-source weather card
ForecastCard.tsx -- 7-day ensemble daily forecast
HourlyForecastCard.tsx -- 48-hour ensemble hourly forecast
AccuracyCard.tsx -- Compact source accuracy summary
AdBanner.tsx -- Self-served privacy-respecting ad
CityAutocomplete.tsx -- Search dropdown with debounce
DataSourceAttribution.tsx -- Per-source attribution display
ErrorBoundary.tsx -- App-level error boundary
screens/
WeatherScreen.tsx -- Main screen: search, median, cards, forecast
SettingsScreen.tsx -- Language, sources, feedback, account
AccuracyScreen.tsx -- Source accuracy leaderboard
LocationPickerScreen.tsx -- City search / GPS picker
PaywallScreen.tsx -- Premium pitch / coming soon page
services/
weatherService.ts -- Calls backend, returns {data, ensemble, errors, skipped, rateLimit}
citySearchService.ts -- Geocoding and reverse geocoding via backend
accuracyService.ts -- Fetches accuracy/leaderboard data
adsService.ts -- Fetches self-served ads and reports clicks
purchaseService.ts -- RevenueCat integration (disabled for launch)
widgets/
widgetTaskHandler.tsx -- Widget lifecycle: ADDED, UPDATE, RESIZED, DELETED, CLICK
widgetDataService.ts -- Fetches widget data from /api/weather/widget
WidgetConfigScreen.tsx -- Per-widget config (city + forecast mode)
WeatherSmallWidget.tsx -- 2x2 widget: temp, city, confidence color
WeatherLargeWidget.tsx -- 4x2 widget: temp, range, 3-day forecast
WeatherDetailedWidget.tsx -- 4x3 widget: full dashboard with 6-day forecast
WeatherOverlayWidget.tsx -- 4x3 transparent overlay widget
theme.ts -- Colors, gradients, day/night helpers
weatherIcons.ts -- Procedural SVG icon library
atoms/ -- Reusable atom components (TemperatureAtom, CityAtom, etc.)
constants/
storage.ts -- AsyncStorage key constants
contexts/
PremiumContext.tsx -- Premium state (isPremium + showAds, both hardcoded true during non-commercial phase)
i18n/
index.ts -- i18next setup, 50 locales, AsyncStorage persistence
languages.ts -- LANGUAGES array (code, name, nativeName)
locales/ -- 50 JSON translation files
utils/
formatCityName.ts -- Display formatter for city + state + country
types/
weather.ts -- WeatherData, WeatherError interfaces
navigation.ts -- React Navigation type definitions
env.d.ts -- @env module type declarations
Backend (backend/)
backend/
src/
index.ts -- Cloudflare Worker entry: routes, handlers, CORS, auth
apis.ts -- Weather API adapters + geocoding functions
ensemble.ts -- Weighted median computation with outlier removal
accuracy.ts -- Forecast accuracy snapshotting and scoring
weather-descriptions.ts -- WMO/Visual Crossing description translation
rate-limiter.ts -- Per-IP rate limiting with SHA-256 hashed IPs
types.ts -- Env, WeatherData, CityResult, EnsembleResult interfaces
schema.sql -- D1 database schema
wrangler.toml -- Cloudflare Workers configuration
Environment and Build
| Concern | Detail |
|---|---|
| Framework | React Native via Expo SDK 54 |
| Backend Runtime | Cloudflare Workers (V8 isolates) |
| Database | Cloudflare D1 (SQLite at the edge) |
| Build System | EAS Build (Expo Application Services) |
| App Env Vars | Loaded via react-native-dotenv Babel plugin, imported from @env |
| Backend Secrets | Set via wrangler secret put (7 API keys, auth secrets, email config) |
| Native Folders | android/ and ios/ are gitignored, managed by EAS Prebuild/CNG |
| Testing | Jest with react-native preset, 80% coverage threshold |