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.

  1. User searches a city (or taps "Use My Location" for GPS reverse-geocode). City autocomplete calls GET /api/geocode?q=... with 300ms debounce.
  2. App calls the backend via POST /api/weather with {lat, lon, name, lang}. Widget requests use the dedicated POST /api/weather/widget endpoint instead.
  3. Backend fans out to all active weather APIs in parallel using Promise.allSettled. Each API adapter normalizes its response to the shared WeatherData interface (all temperatures in Celsius, wind in m/s).
  4. 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.
  5. Backend responds with {data[], ensemble, errors[], skipped[], rateLimit}. data contains per-source results; ensemble contains the aggregated median with confidence metadata; errors lists any sources that failed; skipped lists sources that hit their global API quota.
  6. App renders the ensemble summary at the top, then per-source WeatherCard components, followed by a 7-day ForecastCard.
  7. 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:

Weighted Ensemble Aggregation

The ensemble algorithm in backend/src/ensemble.ts assigns static weights based on forecast model quality:

Source Underlying Model Weight
Open-MeteoECMWF4
OpenWeatherMapGFS + proprietary3
Visual CrossingMulti-model blend2
WeatherAPIMulti-model blend2

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