Anonymous, community-driven real-time incident awareness β on a living map.
π Live Demo Β· πΈ Screenshots Β· π Self-host Β· π€ Contribute
ThreatAlert is a zero-auth, privacy-first PWA where anyone can pin an incident β crime, fire, disaster, civil unrest, or infrastructure failure β on a shared live map, and the community votes on it to verify or resolve it. No sign-up. No tracking. Just signal.
| Feature | Details | |
|---|---|---|
| πΊοΈ | Live Incident Map | Leaflet-powered map with dark & light tile sets, animated category pins, and a pulsing user-location dot |
| π | One-tap Reporting | FAB button, long-press anywhere on the map, or tap-to-place a pin β then pick a category, write a description, and attach up to 3 photos |
| π³οΈ | Community Voting | Confirm Β· Resolve Β· Flag β votes gate incidents from pending β active using per-category thresholds; duplicate votes are blocked server-side |
| π | Push Notifications | Browser push (FCM) with a configurable radius (1 / 5 / 10 / 25 km or worldwide) and minimum-vote threshold |
| π | 3D Globe Gallery | Drag-to-rotate D3.js globe showing every live incident as a glowing dot β tap any dot to open the full detail |
| π° | Incident Ticker | Scrollable strip of active incidents at the bottom of the screen β tap to fly the map to any of them |
| π | Deep-link Sharing | Every incident gets a shareable URL that opens the app, flies to the map location, and shows the detail sheet |
| π² | Installable PWA | Runs standalone on iOS & Android with a home-screen icon and app shortcut for quick reporting |
| π | Dark / Light Theme | System-aware with manual override; map tiles, UI, and globe all switch together |
| π | Privacy-first | Fully anonymous β no accounts, IPs are one-way HMAC-hashed before storage and never logged raw |
| β±οΈ | Auto-expiry | Incidents expire automatically based on category (e.g. Crime: 4 h Β· Fire: 6 h Β· Disaster: 12 h) |
| π‘οΈ | Rate limiting | 5 reports per IP per hour, enforced at the Cloud Functions layer |
Each category has its own colour-coded pin, icon, vote threshold, and time-to-live:
| Icon | Category | TTL | Votes to activate |
|---|---|---|---|
| π‘οΈ | Crime / Safety | 4 h | 3 |
| βοΈ | Natural Disaster | 12 h | 2 |
| π₯ | Fire | 6 h | 2 |
| π§ | Infrastructure | 8 h | 3 |
| π’ | Civil Unrest | 6 h | 4 |
| Other | 4 h | 5 |
βββββββββββββββββββββββββββββββββββββββββββββββ
β Browser / PWA β
β Next.js 16 Β· React 19 Β· Tailwind v4 β
β β
β MapView (Leaflet) β
β IncidentGlobeGallery (D3.js canvas globe) β
β ReportSheet βββββββββββββββββββββββ β
β IncidentDetailSheet β β
β NotificationSheet β β
ββββββββββββββββββββββββββββββββββββββΌβββββββββ
REST / Firestore β FCM push
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Firebase β
β β
β Cloud Functions (Node 20, us-east1) β
β POST /api/createIncident β rate-limit+ β
β POST /api/voteIncident β dedup by IP β
β POST /api/subscribeToAlerts β
β POST /api/unsubscribeFromAlerts β
β β
β Firestore β incidents collection β
β Storage β photo uploads β
β FCM β push notification delivery β
βββββββββββββββββββββββββββββββββββββββββββββββββ
- Writes always go through Cloud Functions (rate-limited, IP-hashed).
- Reads are direct Firestore
onSnapshotstreams β the map updates in real time without polling. - In development with no
NEXT_PUBLIC_FUNCTIONS_URL, the app falls back to direct Firestore writes for easier local testing.
- Node.js 20+
- A Firebase project with Firestore, Storage, and Cloud Messaging enabled
- Firebase CLI:
npm i -g firebase-tools
git clone https://github.com/BaselAshraf81/threatalert.git
cd threatalert
npm installcp .env.local.example .env.localOpen .env.local and fill in your Firebase project values (find them at Firebase Console β Project Settings β Your apps):
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
# VAPID key for Web Push β Firebase Console β Cloud Messaging β Web Push certificates
NEXT_PUBLIC_FIREBASE_VAPID_KEY=
# Salt for one-way IP hashing (never stored raw)
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
IP_HASH_SALT=
ALLOWED_ORIGIN=http://localhost:3000firebase deploy --only firestore:rulescd functions && npm install && cd ..
firebase deploy --only functionsnpm run devOpen http://localhost:3000. The dev server auto-generates the Firebase Messaging service worker before starting.
ThreatAlert ships with configs for both Firebase Hosting and Netlify.
npm run build
firebase deployPush to your repo β netlify.toml handles the build command and publish directory automatically.
Don't forget to set all
NEXT_PUBLIC_*environment variables in your hosting provider's dashboard, and setALLOWED_ORIGINto your production domain.
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY |
β | Firebase web API key |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN |
β | Firebase Auth domain |
NEXT_PUBLIC_FIREBASE_PROJECT_ID |
β | Firestore project ID |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET |
β | Firebase Storage bucket |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID |
β | FCM sender ID |
NEXT_PUBLIC_FIREBASE_APP_ID |
β | Firebase App ID |
NEXT_PUBLIC_FIREBASE_VAPID_KEY |
β | VAPID key for push notifications |
IP_HASH_SALT |
β | Secret for HMAC IP hashing |
ALLOWED_ORIGIN |
β | CORS origin for Cloud Functions |
NEXT_PUBLIC_FUNCTIONS_URL |
Optional | Emulator URL for local function testing |
- No accounts. No email, phone, or OAuth required β ever.
- No raw IPs. Every IP is immediately one-way hashed with a secret salt using HMAC-SHA256 before any Firestore write. The original IP is never stored.
- Rate limiting. Cloud Functions enforce a per-IP, per-hour cap of 5 incident creations and a similar cap on votes to prevent abuse.
- Vote deduplication. The hashed IP is compared server-side to prevent the same person from voting twice on the same incident.
- Input sanitisation. Descriptions are stripped of HTML tags and capped at 280 characters. Photo URLs are validated to only allow Firebase Storage origins before being persisted.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19, Tailwind CSS v4, shadcn/ui, Radix UI |
| Animations | Framer Motion |
| Map | Leaflet + OpenStreetMap / Carto tiles |
| Globe | D3.js (canvas rendering, GeoJSON land features) |
| Backend | Firebase Cloud Functions (Node 20) |
| Database | Firestore (real-time onSnapshot) |
| File storage | Firebase Storage |
| Push | Firebase Cloud Messaging (FCM) + Web Push VAPID |
| PWA | Custom service worker, Web App Manifest |
| Language | TypeScript 5.7 |
| Deploy | Firebase Hosting / Netlify |
Contributions are welcome! Here are some good places to start:
- π Bug reports β open an issue with reproduction steps
- π‘ Feature requests β open a discussion first so we can align on direction
- π§ Pull requests β please open an issue or discussion before large changes
# Fork the repo, then:
git checkout -b feat/your-feature
npm run dev
# make your changes
npm run lint
git commit -m "feat: describe your change"
git push origin feat/your-feature
# open a PR πMIT β see LICENSE for details.
Built with π΄ for safer, more aware communities.
β Star this repo if you find it useful β it helps more people discover ThreatAlert!
