Real‑time heatmap and chat explorer for Nostr geohash activity (kind 20000). The app ingests public relay traffic over WebSocket, aggregates activity by geohash, and visualizes hotspots on a Leaflet map with a Discord‑style channel sidebar and a searchable chat timeline.
Highlights
- Dual/Chat/Map layouts with a single click view switcher (click the same view again to revert to your previous view)
- Always‑visible, modern, fixed‑width sidebar in Dual view (collapses automatically in Chat/Map)
- Vibrant heatmap with numeric badges showing unique users per geohash (not message count)
- Click a heat badge to jump to that geohash channel and focus the map
- Chat timeline with pause and auto‑scroll controls, and an inline search bar (🔍) above the general header
- Resilient WebSocket client with reconnection and optional initial cache fetch
- Production build served by a Node/Express server that also handles a WebSocket fan‑out and REST API
- Frontend: Vite + React 18 + TypeScript
- Map: Leaflet + CartoDB dark tiles +
leaflet.heat - Backend: Node.js + Express +
wsWebSocket server - Data: Nostr relays (configurable) with in‑memory cache (
node-cache)
client (vite dev) <—ws/http—> server (express + ws)
↳ React app renders map + chat ↳ connects to relays, caches, broadcasts
/ (project root)
├─ server.js # Express + ws server, REST API, Nostr relay ingestion
├─ src/
│ ├─ components/
│ │ ├─ ChannelSidebar.tsx # Modern sidebar (geohashes as channels)
│ │ ├─ ChatTimeline.tsx # Timeline with pause/autoscroll + 🔍 search
│ │ └─ HeatmapView.tsx # Leaflet map + heat layer + badges
│ ├─ hooks/
│ │ └─ useWebSocket.ts # Client WebSocket lifecycle + reconnection
│ ├─ types/ # Shared TS types
│ └─ utils/ # Helpers (time, etc.)
├─ public/ # Legacy demo UI (not used by the React app)
├─ scripts/
│ └─ seed-activity.js # Local seeding helper to publish sample geohash events
├─ index.html # Vite entry (React)
├─ vite.config.ts # Vite + dev proxy for /api and /ws
├─ package.json # Scripts + deps
└─ README.md # You are here
- Node.js 18+
- npm 9+
Note: If you fork this repo and the images don’t render on GitHub, add your screenshots at:
- assets/screenshots/dual-view.png
- assets/screenshots/map-view.png
# 1) Install deps
npm install
# 2) Run in development (Vite + Nodemon)
npm run dev
# 3) Seed some sample activity (optional, so you can see hotspots)
npm run seed # sends ~20 random geohash events to the local serverDevelopment scripts
npm run dev # concurrently runs server (nodemon) + client (vite)
npm run build # vite build -> dist/
npm start # serve production build with Node server.js
npm run preview # vite preview (static)
npm run seed # publish random geohash events to local API
npx tsc --noEmit # typecheckEnvironment variables (optional):
CACHE_TTL(seconds, default300) – in‑memory cache retentionNOSTR_RELAYS– comma‑separated list of relay URLs. Defaults:wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net,wss://offchain.pub,wss://nostr21.com,wss://nostr-pub.wellorder.net,wss://nostr.wine
Create a .env (optional):
CACHE_TTL=300
NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol
Vite dev server proxies API and WebSocket to the Node server (see vite.config.ts). In the browser, the client connects to ws(s)://<host>/ws.
- Server connects to configured Nostr relays and subscribes to kind
20000events (geohash activity). - It verifies events, caches recent activity per geohash, and broadcasts new activity to connected WebSocket clients.
- The React app renders:
- Leaflet map with a vibrant heat layer and numeric badges for unique users per geohash.
- A modern sidebar listing active geohash channels.
- A chat timeline that updates live, with pause/auto‑scroll/search controls.
- Clicking a map badge or a channel focuses the map and filters the chat timeline to that geohash.
Unique‑user counts
- Heat intensity and badge numbers reflect unique users (by npub fallback nickname) per geohash – not raw message count.
View switcher
Dual– Map and chat side‑by‑side. Sidebar is pinned and fully visible.Chat– Only chat; sidebar collapses automatically.Map– Only map; sidebar collapses automatically.- Clicking the same view again toggles back to your previous view.
The server exposes a small set of endpoints (see server.js):
GET /api/activity– recent geohash activities (flattened)GET /api/heatmap-data– aggregate heatmap data by geohashGET /api/relays– relay connection statusGET /api/coolr-test/:relay/:channel– helper for coolr.chat URLsPOST /api/keys/generate– generate Nostr keys (npub/nsec)POST /api/keys/decode– decode npub/nsecPOST /api/lightning/validate– validate a Lightning address (lnurl)POST /api/events/publish– publish a kind20000geohash event via local serverPOST /api/zaps/create-request– create a NIP‑57 zap request payloadGET /api/users/:npub– aggregate recent activity for a user
The seed script publishes random events to your local server so you can see the map light up right away.
npm run seed
# or:
npm run seed -- 50 # send 50 eventsThe seeder generates a random 32‑byte hex private key locally (no external reliance) and posts to /api/events/publish.
npm run build # produces dist/
npm start # node server.js (serves dist/ + ws + API)Deploy anywhere you can run Node (Railway, Fly.io, Render, Heroku, a VM). Ensure websockets are enabled. Set NOSTR_RELAYS and PORT as needed.
- Change the brand/title in
src/App.tsxand theme insrc/App.css(CSS variables at the top). - Sidebar behavior: the sidebar is forced open in Dual view (see
App.tsx), and collapses automatically in Chat/Map. - Map tiles: switch the basemap by editing the tile layer in
HeatmapView.tsx. - Heat gradient and badge thresholds: tunable in
HeatmapView.tsx. - Relay set: define
NOSTR_RELAYSin.envor your hosting provider.
When you publish your fork
- Update this README’s title and links.
- Consider adding a screenshot and a short demo clip.
- “Tiles not visible” – We force normal blend mode for Leaflet tiles and use CartoDB dark tiles. If you swap tiles, ensure no CSS blend rules conflict.
- WebSocket not connecting in dev – Ensure
vite.config.tsproxy has/wswithws: true, and the server is listening on3001. - No data – Run
npm run seedor confirm your relay list is reachable from your server.
MIT – see LICENSE (add one if missing).
- Leaflet,
leaflet.heat, Carto basemaps - Nostr protocol and the public relay ecosystem

