Watch how a natural event on one side of the world ripples out to the prices and products in your everyday life.
🌐 Live demo → ripple-taupe.vercel.app
Ripple plots live natural events on a dark world map and animates the supply-chain ripples from each event out to the production regions, commodities, and market prices it affects — then makes it personal with a price-watch basket and a near-term cost-pressure forecast for your location.
⏳ The API runs on a free tier that sleeps when idle, so the first load after a quiet period can take ~30–50s to wake up. Subsequent loads are instant.
- Live global events merged from three feeds (NASA EONET · USGS · GDACS), with magnitude-sized markers
- Ripple arcs — event → nearest affected production regions → at-risk commodities, with an animated "comet" on the selected event
- Live commodity prices (Yahoo Finance) rendered as hard-edged brutalist sparklines
- Commodity lens — "show everything affecting coffee"
- Today's Biggest Ripple hero + downloadable share card
- My Basket + Ripple Forecast — pick everyday goods, get a personal near-term cost-pressure signal (RISING / WATCH / STABLE / EASING) from price momentum + active event threats, tied to your auto-detected location
- Impact explainer — template baseline, optional Gemini overlay, cached per event
- Neo-brutalist UI, fully responsive (mobile → desktop)
| Layer | Choice |
|---|---|
| Frontend | React + TypeScript, Vite, deck.gl (ArcLayer/ScatterplotLayer), MapLibre GL (react-map-gl), Tailwind CSS v4, Framer Motion, TanStack Query, Zustand, lucide-react, html-to-image |
| Backend | Go — chi router, database/sql, go:embed; concurrent multi-source ingester with on-demand caching |
| Database | SQLite locally (pure-Go modernc.org/sqlite) · Supabase Postgres (pgx/v5) in production — driver chosen by DATABASE_URL |
| Hosting | Vercel (frontend) · Render (backend) · Supabase (DB) — all free tier |
The frontend talks to the Go API for all core data; a few keyless client-side lookups (map tiles, geolocation) are noted below.
Runs entirely on free, open data sources — no paid API keys required (Gemini is the only optional key, and it falls back to templates without one).
Backend (server-side):
| API | Use |
|---|---|
| NASA EONET v3 | Wildfires, storms, volcanoes, floods, etc. |
| USGS Earthquakes | M4.5+ weekly GeoJSON feed |
| GDACS | Global disaster alerts |
| Yahoo Finance chart API | Commodity price series + % change |
| Google Gemini (optional) | Impact-explainer text (falls back to templates) |
Frontend (client-side):
| API | Use |
|---|---|
| CARTO dark raster tiles | Map basemap |
| Browser Geolocation | Exact location (opt-in) |
| BigDataCloud | Reverse-geocode coords → city name |
| ipwho.is · geojs.io · ipapi.co | IP-based location fallback chain |
NASA EONET ┐
USGS ├─▶ Go ingester (concurrent, on-demand cache) ─▶ Postgres/SQLite
GDACS ┘ │
▼
Browser ◀── React + deck.gl/MapLibre ◀────────── Go API (chi) ──┘
(map, ripples, forecast, basket) /api/events · /api/ripples
/api/prices · /api/events/{id}
Events are cached in the DB and refreshed on demand (default TTL 15 min) — no background worker, so it behaves correctly on free hosts that sleep when idle.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/api/events?status=&category=&limit= |
Events as a GeoJSON FeatureCollection |
GET |
/api/events/{id} |
One event's detail (lazily generates + caches the impact explainer) |
GET |
/api/ripples?status= |
Ripple arcs: event → affected regions + at-risk commodities |
GET |
/api/prices?tickers=KC=F,CL=F |
Recent price trend per ticker |
POST |
/api/ingest |
Force a refetch from all sources |
Requires Go 1.25+ and Node 20+. No database setup needed (defaults to a local SQLite file).
# Terminal 1 — backend
cd backend
cp .env.example .env # optional; defaults are fine
go run ./cmd/server # http://localhost:8080
# Terminal 2 — frontend
cd frontend
npm install
npm run dev # http://localhost:5173Vite proxies /api → :8080 in dev, so there's no CORS setup locally.
Backend env (see backend/.env.example):
| Var | Default | Notes |
|---|---|---|
PORT |
8080 |
Set automatically by Render |
DATABASE_URL |
sqlite://ripple.db |
Use a postgres://… URL for Supabase |
CORS_ORIGINS |
http://localhost:5173,… |
Comma-separated allowed origins |
EVENTS_CACHE_TTL_MINUTES |
15 |
How long before a refetch |
AI_PROVIDER |
template |
template | gemini | groq |
Frontend env (frontend/.env.example): VITE_API_BASE — empty in dev; set to the deployed API URL in production.
- Backend → Render via the
render.yamlblueprint (setDATABASE_URL,CORS_ORIGINS) - Database → Supabase (Session pooler connection string; the backend auto-creates its table on first boot — no migrations to run)
- Frontend → Vercel (root directory
frontend, envVITE_API_BASE= the Render URL)
Ripple/
├─ render.yaml # Render blueprint (backend)
├─ PROJECT_PLAN.md # concept, data sources, roadmap
├─ backend/ # Go API
│ ├─ cmd/server/ # entrypoint
│ └─ internal/
│ ├─ config/ db/ api/ # config, store+migration, chi handlers
│ ├─ eonet/ usgs/ gdacs/# data-source clients
│ ├─ ingest/ # concurrent on-demand cache
│ ├─ ripple/ prices/ impact/ # ripple engine, price client, explainer
│ └─ models/
└─ frontend/ # React + deck.gl
└─ src/
├─ components/ # MapView, EventPanel, MyBasket, FilterBar, ui/*
├─ lib/ # api client, forecast, location, categories
└─ store.ts # zustand UI state