Global disease surveillance, visualized like a weather radar.
Track outbreaks. Monitor spread patterns. WHO Disease Outbreak News on a 6-hour Vercel Cron cycle when you run it against your own Supabase project. The public demo serves the cached static snapshot.
Disease outbreaks don't announce themselves neatly. Data is scattered across WHO bulletins, news wires, and government reports. PulseMap consolidates it into a single dark-mode map where outbreak density glows like radar returns β green for low concern, red for critical.
This isn't a wrapper around an API. It's a full data pipeline: ingestion from WHO Disease Outbreak News, geocoding, severity estimation, deduplication, and a live-updating frontend that makes the data feel urgent.
| Feature | Detail |
|---|---|
| Heat map layer | Outbreak intensity rendered as weighted heatmap β green β yellow β red gradient by severity score |
| Pulsing hotspot markers | Log-scaled by case count, color-coded by severity, animated CSS pulse rings |
| Click-to-detail panels | Disease name, case count, severity classification, WHO summary text |
| Stats bar | Aggregate dashboard strip β outbreak count, total cases, countries affected, critical alerts, severity distribution, top disease |
| Live feed | Chronological outbreak reports with source badges β scrollable, searchable |
| Search | Filter map + feed by disease name or country |
| Timeline slider | Draggable temporal filter with sparkline density visualization β scrub through outbreak history, all layers + feed + stats update in real-time |
| Spread network | Animated great-circle arcs connecting outbreak locations of the same disease β glowing lines with flowing dash animation, color-coded by severity |
| Layer toggles | Independent visibility controls for heatmap, hotspot, and spread layers |
| Data source indicator | Real-time badge showing whether you're seeing live Supabase data or static fallback |
| Fly-to navigation | Click a feed item β map smoothly flies to the outbreak location |
ββββββββββββββββββββββββββββββββββββ
β Vercel Edge Network β
β (Next.js 16 App Router) β
ββββββββββββ¬ββββββββββββ¬βββββββββββββ€
β Mapbox β React UI β API Layer β
β GL JS β 8 comps β 2 routes β
β dark-v11β client β server β
ββββββββββββ΄ββββββββββββ΄βββββββββββββ€
β Supabase (Postgres) β
β RLS policies Β· Realtime-ready β
ββββββββββββββββββββββββββββββββββββ€
Every 6h ββββββββΆβ WHO Disease Outbreak News API β
POST /backfill ββΆβ Geocoding (Mapbox + static) β
β Deduplication Β· Severity scoring β
ββββββββββββββββββββββββββββββββββββ
Data flow: WHO API β fetch-outbreaks.ts pipeline β geocode country/region β estimate severity β upsert to Supabase β GeoJSON served to Mapbox GL heatmap + marker layers.
| Layer | Choice | Rationale |
|---|---|---|
| Framework | Next.js 16 (App Router) | SSR + server routes + Vercel-native cron |
| UI | React 19 + TypeScript | Strict typing across all 8 components |
| Styling | Tailwind CSS 4 | Custom dark theme, no default palette |
| Map | Mapbox GL JS | Heatmap layers, smooth fly-to, dark basemap |
| Database | Supabase (Postgres) | Row-level security, realtime subscriptions |
| Data Source | WHO DON API | Structured, authoritative, global coverage |
| Geocoding | Mapbox Geocoding + static lookup | Hybrid: API for precision, static for speed |
| Testing | Vitest | 64 unit tests across data ingestion, WHO parser, spread arcs, JSON validation |
| Hosting | Vercel | Auto-deploy, cron scheduling, edge CDN |
- Node.js 18+
- Mapbox account (free tier works)
- Supabase project (free tier works)
git clone https://github.com/DareDev256/pulsemap.git
cd pulsemap
npm installCreate .env.local from the example:
cp .env.local.example .env.localRequired variables:
NEXT_PUBLIC_MAPBOX_TOKEN= # Mapbox GL access token
NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=# Supabase anon/public key
SUPABASE_SERVICE_ROLE_KEY= # Supabase service role (server-side only)
CRON_SECRET= # Auth token for cron + backfill endpointsRun the migration in your Supabase SQL editor:
# Copy contents of supabase/migration-001.sql into Supabase SQL Editor β RunOutbreak records come from WHO Disease Outbreak News. When Supabase is unreachable the UI
serves src/lib/seed-data.ts, a static snapshot, and the data-source badge in the header
tells you which of the two you are looking at. The public demo is currently on that snapshot.
# Pull current outbreaks
curl http://localhost:3000/api/cron/update-outbreaks \
-H "Authorization: Bearer $CRON_SECRET"
# Backfill a date range
curl -X POST http://localhost:3000/api/backfill \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json" \
-d '{"startDate":"2024-01-01","endDate":"2024-12-31","source":"who","limit":200}'| Parameter | Type | Required | Description |
|---|---|---|---|
startDate |
string | Yes | Range start (YYYY-MM-DD) |
endDate |
string | Yes | Range end (YYYY-MM-DD) |
source |
string | No | who or all (default: who) |
limit |
number | No | Max reports (default: 200, max: 500) |
npm run dev # Development server
npm run build # Production build
npm test # Run test suite (64 tests)
npm run test:watch # Watch modeResponse headers (all routes):
- Content Security Policy -- script/connect/style origins allowlisted per-service (Mapbox, Supabase, WHO, Google Fonts). Everything else blocked.
- X-Frame-Options: DENY +
frame-ancestors 'none'-- prevents clickjacking - HSTS -- enforces HTTPS for 1 year including subdomains
- X-Content-Type-Options / Referrer-Policy / Permissions-Policy -- blocks MIME sniffing, controls Referer leakage, disables unused browser APIs
API authentication (/api/cron/*, /api/backfill):
CRON_SECRETis mandatory -- requests are rejected with503when the env var is missing, not silently allowed through- Timing-safe token comparison -- Bearer tokens are compared in constant time to prevent timing attacks
- Error sanitization -- API errors never leak stack traces or internal paths to clients
- Input allowlisting -- the
sourceparameter is validated against a strict allowlist - Centralized input validation -- backfill endpoint enforces Content-Type, safe JSON parsing, calendar-valid dates (round-trip check rejects
02-30), 365-day max range, future date rejection, and strict numeric limit typing - Upstream timeouts -- all external API fetches use
AbortControllerwith a 15s deadline - Fail-fast env validation -- server-side Supabase client throws immediately if credentials are missing instead of failing silently
src/
βββ app/
β βββ api/
β β βββ cron/ # Vercel Cron job β WHO ingestion every 6h
β β βββ backfill/ # POST endpoint β historical data by date range
β βββ page.tsx # Main dashboard (client component)
β βββ globals.css # Dark theme + hotspot marker animations
βββ components/
β βββ TimelineSlider.tsx # Temporal filter with sparkline + range input
β βββ PulseMap.tsx # Mapbox GL map with heatmap + markers
β βββ Feed.tsx # Scrollable outbreak feed
β βββ Navbar.tsx # Top bar with search
β βββ LayerControls.tsx # Heatmap/hotspot toggle panel
β βββ Legend.tsx # Color severity legend
β βββ OutbreakDetail.tsx # Click-to-detail side panel
βββ lib/
β βββ api-client.ts # Dashboard data loader (single entry point)
β βββ fetch-outbreaks.ts # Supabase query β GeoJSON transform
β βββ spread-arcs.ts # Great-circle arc generator for disease networks
β βββ pipeline/ # WHO API client + geocoding + dedup
β βββ seed-data.ts # Static fallback data
β βββ supabase.ts # Client initialization
βββ types/ # Shared TypeScript interfaces
- Phase 1 β Interactive heatmap + pulsing hotspot markers on dark basemap
- Phase 1 β Weather-station UI: feed panel, detail drawer, severity legend
- Phase 1 β Supabase backend with RLS policies
- Phase 4 β Automated WHO pipeline via Vercel Cron (6h cycle)
- Phase 4 β Geocoding, deduplication, severity estimation
- Phase 4 β Historical backfill API endpoint
- Phase 2 β Disease spread network with animated great-circle arcs
- Phase 2 β News pin layer (geolocated news markers, toggleable)
- Phase 3 β Google OAuth + user preferences (saved views, "near me")
- Phase 3 β Push notifications for outbreaks in your region
- Phase 4+ β CDC API as secondary data source
- Phase 4+ β LLM-powered extraction from general news APIs
- Phase 5 β Community reporting + moderation + trust scoring
- Phase 5 β Real-time Supabase subscriptions
Historical timeline sliderβ Shipped in v0.4.0- Mobile-responsive layout + PWA offline access
| Source | Status | Frequency |
|---|---|---|
| WHO Disease Outbreak News | Active | Every 6 hours |
| CDC | Planned | β |
| ReliefWeb | Planned | β |
| Community Reports | Planned (Phase 5) | Real-time |
MIT
Built by @DareDev256