
CONFIDENTIAL TECHNICAL OVERVIEW • MARCH 2026
Prepared for Stuart Mitchell — Defensible Space & Structure Hardening Specialist
EmberWatch is Prodigy's real-time wildfire intelligence layer. It continuously ingests fire data from10 government and satellite sources across 3 countries, normalizes it into a unified schema, and delivers property-specific threat assessments to homeowners — typically within15–30 minutes of fire detection.
The system runs autonomously 24/7 — no human operator. A scheduled job fetches, deduplicates, and caches fire data every 5 minutes. Users never hit external APIs directly; they read from a pre-built cache that delivers sub-200ms response times regardless of how many people are watching the same fire.
We pull from 10 live government feeds, organized into two parallel fetch batches to manage memory (each serverless function has a 128MB limit):
National Interagency Fire Center — the authoritative US wildfire incident database. ArcGIS FeatureServer, up to 1,500 records. Includes IRWIN IDs, containment %, fire cause, jurisdictional agency, and incident type classification.
services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/WFIGS_Incident_Locations_Current
Canadian Wildland Fire Information System — national feed via GeoServer WFS. Reports fire stage of control (Out of Control, Being Held, Under Control, Extinguished), hectares burned, and provincial agency.
cwfis.cfs.nrcan.gc.ca/geoserver/public/ows
BC Wildfire Service — ArcGIS endpoint for British Columbia specifically. Critical for Prodigy's core market. Includes fire cause, geographic description, and BC-specific fire number identifiers.
services6.arcgis.com/ubm4tcTYICKBpist/arcgis/rest/services/BCWS_ActiveFires_PublicView
New South Wales Rural Fire Service — major incidents JSON feed. Handles polygon geometries (centroid extraction for point-based mapping). Categorizes hazard reduction burns separately.
rfs.nsw.gov.au/feeds/majorIncidents.json
Emergency Victoria — GeoJSON feed filtered for fire/bushfire categories. Maps alert levels (Safe → Watch and Act → Emergency Warning) to our normalized status enum.
emergency.vic.gov.au/public/events-geojson.json
Queensland Fire and Emergency Services — S3-hosted JSON feed for bushfire current incidents.
publiccontent-gis-psba-qld-gov-au.s3.amazonaws.com
SA Country Fire Service — CRIIMSON incident feed. Filtered for fire/burn incident types.
data.eso.sa.gov.au/prod/cfs/criimson/cfs_current_incidents.json
NT Police, Fire & Emergency Services — GeoJSON incidents feed. Distinguishes planned burns from wildfires.
pfes.nt.gov.au/incidentmap/json/incidents.json
WA Dept of Fire & Emergency Services — REST API with incident objects. Includes suburb-level locality data.
api.emergency.wa.gov.au/v1/incidents
Tasmania Fire Service — JSON incident feed filtered for fire events.
fire.tas.gov.au
DataSourceHealth entity (latency, consecutive failures, last success time) so we can monitor reliability over time.Every data source returns a different schema. The pipeline normalizes each fire into aunified record with consistent fields:
// Normalized fire record structure:
{
fire_id: "nifc_ABC123", // Globally unique: source prefix + native ID
region: "US", // US | CA | AU
source: "NIFC", // Which API provided this
name: "Oak Fire", // Human-readable name
latitude / longitude: 34.12, -119.45, // WGS84 coordinates
// Status normalization — every source maps to ONE of these:
normalized_status: "OUT_OF_CONTROL", // OUT_OF_CONTROL | BEING_HELD | UNDER_CONTROL | UNKNOWN
status_raw: "OC", // Original value from source API
// Classification
is_prescribed: false, // Prescribed/hazard reduction burns flagged separately
is_controlled: false, // 100% contained
is_hotspot: false, // Satellite thermal detection vs confirmed incident
fire_type: "WILDFIRE", // WILDFIRE | PRESCRIBED | HOTSPOT
containment_pct: 35, // 0-100 (null if unavailable)
// Size & timing
size_hectares: 5000,
discovered_date: "2026-03-15T08:00Z",
// Metadata (varies by source)
metadata: {
agency: "USFS", // Jurisdictional agency
cause: "Lightning", // Fire cause if reported
state: "CA", county: "Ventura", // US location detail
province: null, // Canadian province
}
}| Source | Raw Value | Normalized |
|---|---|---|
| NIFC | PercentContained: 100 | UNDER_CONTROL |
| CWFIS | stage_of_control: "OC" | OUT_OF_CONTROL |
| BCWS | FIRE_STATUS: "Being Held" | BEING_HELD |
| NSW RFS | status: "Out of control" | OUT_OF_CONTROL |
| VIC Emergency | status: "Watch and Act" | BEING_HELD |
| QLD QFES | Status: "Controlled" | UNDER_CONTROL |
We flag prescribed/hazard reduction burns at ingestion time to prevent false alarms:
Prescribed burns are never promoted to critical alerts. They have a 7-day age limit vs 21–30 days for wildfires.
With 10 sources, the same fire can appear multiple times (e.g., CWFIS and BCWS both reporting the same BC fire). We use fire_id as a composite key (source prefix + native ID) and merge into a HashMap — first-write wins.
Stale fires are automatically removed based on type and age:
Prescribed burns: ≤ 7 days old
Controlled (100%): ≤ 10 days old
NIFC incidents: ≤ 30 days old // US fires have slower reporting cycles
All other wildfires: ≤ 21 days old
// Special case: NIFC fires with no discovery date are kept
// if they have size > 1ha OR containment < 100%If the new fire count drops below 10% of the previous count (and previous was >50), the system keeps the old cache instead of overwriting with potentially bad data. This protects against upstream API outages delivering empty or partial datasets.
┌─────────────────────────────────────────────────────────────────┐
│ LAYER 1: FireCache Entity (Database) │
│ Updated every 5 min by refreshFireCache scheduled job │
│ Contains ALL fires globally (typically 400-800 records) │
│ Single row: cache_key = "global_fires" │
└──────────────────────────┬──────────────────────────────────────┘
│ DB read (once per 30s per isolate)
┌──────────────────────────▼──────────────────────────────────────┐
│ LAYER 2: In-Memory Cache (Deno Deploy Isolate) │
│ 30-second TTL — shared across all concurrent requests │
│ Request coalescing: 1000 simultaneous users → 1 DB read │
│ Bounding-box pre-filter → O(relevant) not O(all) │
└──────────────────────────┬──────────────────────────────────────┘
│ JSON response (~10KB per user)
┌──────────────────────────▼──────────────────────────────────────┐
│ LAYER 3: Client Session Cache (Browser RAM) │
│ Push-based reactive cache — updates on Layout prefetch │
│ Instant restore on tab switch (0ms) │
└──────────────────────────┬──────────────────────────────────────┘
│ Fallback if offline
┌──────────────────────────▼──────────────────────────────────────┐
│ LAYER 4: localStorage Cache (5-min TTL) │
│ Enables offline mode — stale data better than no data │
│ Auto-cleared when fresh data arrives │
└─────────────────────────────────────────────────────────────────┘After the cache is written, the system runs a movement tracking phase. For every property with coordinates, it checks each nearby fire against previously recorded distances.
// Movement detection logic:
previousDistance = FireTracking.distance_km // from 5 min ago
currentDistance = haversine(property, fire) // now
distanceChange = current - previous
if (|distanceChange| < 0.5 km) → "stable"
if (distanceChange < 0) → "approaching" // GETTING CLOSER
if (distanceChange > 0) → "receding" // MOVING AWAY
// If approaching:
approachRate = |distanceChange| / hoursSinceLastCheck // km/h
estimatedArrival = currentDistance / approachRate // hoursEach fire gets a 0–100 threat score based on distance + movement:
| Factor | Points |
|---|---|
| Within 10 km | +40 |
| Within 25 km | +30 |
| Within 50 km | +20 |
| Approaching | +20 |
| Approach rate > 5 km/h | +10 |
Alert levels drive notifications (push, email, SMS):
Alerts are only generated when status changes (e.g., a fire transitions from "stable" to "approaching", or from WARNING to CRITICAL). This prevents notification spam during ongoing situations.
Real-time weather from Open-Meteo (free, no API key, global coverage). Per-location, 10-minute cache. Fields: temperature, humidity, wind speed, wind gusts, wind direction.
The risk score is a two-stage process:
Stage 1: Deterministic Score — hard-coded rules, no AI involved:
// Fire proximity (confirmed incidents only — hotspots excluded)
≤5 km → base 85-100 (CRITICAL regardless of weather)
5-15km → base 50-85 (HIGH, scaled by distance)
15-30km → base 25-40 (MODERATE, weather can increase)
30-50km → base 15 (LOW, weather-driven)
// Temperature modifiers
≤0°C: cap score at 8 (or 70 if confirmed fire ≤10km)
≤10°C: cap score at 15 (or 60 if confirmed fire ≤15km)
≥38°C: +20 points
≥32°C: +12 points
// Humidity modifiers
≤15%: +15 points (critically dry)
≤25%: +10 points
// Wind modifiers
≥50 km/h: +20 points (extreme)
≥30 km/h: +10 pointsStage 2: AI Contextual Analysis — the deterministic score is passed to an LLM with instructions to use the exact score but provide contextual explanation, actionable advice, fire spread prediction based on wind direction, and sprinkler system recommendations. Results are cached for 6 hours per risk-score band.
The EmberWatch map renders multiple overlay layers on Leaflet + Stadia Maps dark tiles:
Fire Movement Detected
│
▼
Alert Level Changed? ─── No ──→ Skip (no spam)
│
Yes
│
▼
Is Prescribed Burn? ─── Yes ──→ Skip (never alert on Rx)
│
No
│
▼
Is Test Mode? ─── Yes ──→ Skip (demo safety)
│
No
├──→ In-App Notification (always)
├──→ Push Notification (native APNs / Firebase)
├──→ Email (Resend API — fallback or if enabled)
└──→ SMS (Twilio — if phone verified & enabled)| Component | Technology | Purpose |
|---|---|---|
| refreshFireCache | Deno serverless (scheduled) | Fetch + normalize + dedupe + cache every 5 min |
| getFireSnapshot | Deno serverless (on-demand) | Read cache, filter by location, serve to users |
| fetchWeather | Open-Meteo API | Real-time weather per property (10min cache) |
| analyzeHazardRisk | Deterministic + LLM | Risk scoring + contextual AI analysis |
| triggerEmberWatchAlert | Push + Email + SMS | Multi-channel alert delivery |
| FireCache entity | NoSQL database | Global fire cache (single row, all fires) |
| FireTracking entity | NoSQL database | Per-property fire distance tracking over time |
| DataSourceHealth entity | NoSQL database | API health monitoring per source |
© 2026 Prodigy Wildfire Solutions. This document is confidential and intended for Stuart Mitchell only.
Fire data sourced from public government feeds. Prodigy is not affiliated with any government entity.
Taking longer than usual to load. Check your connection and try again.