A citizen-driven emergency reporting platform built for Venezuela. Reports arrive via Telegram and WhatsApp, are triaged by AI, geo-referenced, and displayed on a live map.
π Live: pulsoaid.org
PULSO is an open-source emergency coordination platform that aggregates real-time citizen reports during crises (earthquakes, floods, blackouts, medical emergencies). It processes incoming messages from Telegram and WhatsApp, uses Google Gemini to classify and extract structured data (location, severity, type), and displays everything on a geo-referenced interactive map β accessible offline as a Progressive Web App.
[Telegram / WhatsApp]
β
βΌ
[IngressApi ββββββββββββββββββββββββββββββββββββββββββββββββββ]
β β’ Validates & queues incoming reports β
β β’ Serves situations, sectors, SSE stream, comments API β
β β’ Rate-limited per IP β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β Redis Queue
βΌ
[AiWorker βββββββββββββββββββββββββββββββββββββββββββββββββββββ]
β β’ Dequeues reports β
β β’ Classifies with Google Gemini (severity, type, summary) β
β β’ Geo-codes with LLM + Nominatim fallback (PostGIS) β
β β’ Persists to PostgreSQL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
[PostgreSQL / Supabase (PostGIS)]
β
ββββββββββββββββ
βΌ
[PWA (Svelte + Leaflet) βββ SSE /stream ββ IngressApi]
β’ Offline-first (Service Worker)
β’ Cluster map with real-time updates
β’ Anonymous citizen comments per incident
| Layer | Technology |
|---|---|
| API & Worker | .NET 10, ASP.NET Core Minimal APIs (Slim Builder) |
| Database | PostgreSQL via Supabase (PostGIS extension) |
| Queue | Redis (StackExchange.Redis) |
| AI Triage | Google Gemini (structured output) |
| Geocoding | LLM primary + Nominatim/OSM fallback |
| Real-time | Server-Sent Events (SSE) |
| Frontend | Svelte 4, Vite 5, Leaflet, Dexie (IndexedDB) |
| PWA | vite-plugin-pwa (Workbox, offline-first) |
| Reverse Proxy | Caddy (auto-HTTPS, SSE bypass, cache headers) |
| Observability | OpenTelemetry β OTLP (traces, metrics, logs) |
| Messaging | Telegram Bot API, WhatsApp (webhook) |
| CI/CD | GitHub Actions (staging + production) |
- .NET 10 SDK
- Node.js 20+ + pnpm
- A Supabase project (PostgreSQL + PostGIS)
- A Redis instance
Local configurations are managed using .NET User Secrets (recommended) or environment variables. Do not use the pulso-*.service.template files for local development; they are systemd unit files intended for VPS staging/production deployments.
Initialize and set the required keys for both projects:
cd src/Pulso.IngressApi
dotnet user-secrets init
# Set required database, Redis, and Telegram secret webhook tokens:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=TU_DB_HOST;Port=6543;Database=postgres;Username=postgres.TU_PROJECT_ID;Password=TU_DB_PASSWORD;SslMode=Require;Trust Server Certificate=true;No Reset On Close=true;"
dotnet user-secrets set "ConnectionStrings:UpstashRedis" "localhost:6379,password=TU_REDIS_PASSWORD,abortConnect=false"
dotnet user-secrets set "Telegram:SecretToken" "TU_TELEGRAM_SECRET_TOKEN"cd src/Pulso.AiWorker
dotnet user-secrets init
# Set required database, Redis, Gemini, and Telegram bot tokens:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=TU_DB_HOST;Port=6543;Database=postgres;Username=postgres.TU_PROJECT_ID;Password=TU_DB_PASSWORD;SslMode=Require;Trust Server Certificate=true;No Reset On Close=true;"
dotnet user-secrets set "ConnectionStrings:UpstashRedis" "localhost:6379,password=TU_REDIS_PASSWORD,abortConnect=false"
dotnet user-secrets set "Telegram:BotToken" "TU_TELEGRAM_BOT_TOKEN"
dotnet user-secrets set "GeminiApiKey" "TU_GEMINI_API_KEY"
# Required only to upload photos attached to reports (e.g. pet reports) to Supabase Storage:
dotnet user-secrets set "Supabase:Url" "https://TU_PROJECT_ID.supabase.co"
dotnet user-secrets set "Supabase:ServiceRoleKey" "TU_SUPABASE_SERVICE_ROLE_KEY"
dotnet user-secrets set "Supabase:BucketName" "reports"# Using the Supabase CLI
supabase db pushOr apply the SQL files in supabase/migrations/ manually in order.
# IngressApi (runs on port 5152 by default)
dotnet run --project src/Pulso.IngressApi
# AiWorker (background queue processor)
dotnet run --project src/Pulso.AiWorkerThe PWA frontend development server proxies /api requests to http://localhost:5152 (configured in vite.config.js).
cd src/Pulso.Pwa
pnpm install
pnpm devThe project deploys automatically via GitHub Actions on push to stage (staging) and main (production). See the workflow files in .github/workflows/ for details.
PULSO exposes a public, open, read-only API so anyone can extract the collected reports β no API key required. The goal is to keep the information open rather than privatized.
| Resource | Description |
|---|---|
GET /api/v1/public/incidents |
Paginated record-level export (JSON, or GeoJSON via Accept: application/geo+json) |
GET /api/v1/public/incidents/{id} |
A single incident record by id |
GET /api/v1/public/incidents/{id}/comments |
Comments/updates posted on an incident |
GET /api/v1/public/openapi.yaml |
Hand-written OpenAPI 3.1 specification (the contract) |
GET /api/v1/public/docs |
Interactive API documentation |
Incremental sync uses a stable opaque cursor over (created_at, id):
# First page (oldest first)
curl "https://pulsoaid.org/api/v1/public/incidents?limit=1000"
# Follow pagination.next_cursor until has_more is false
curl "https://pulsoaid.org/api/v1/public/incidents?since=<next_cursor>&limit=1000"
# GeoJSON for QGIS / Leaflet / Mapbox
curl -H "Accept: application/geo+json" "https://pulsoaid.org/api/v1/public/incidents"Privacy: report content (text, transcriptions, declared location, GPS, and the names of missing/found people) is public by design. The reporter's phone number and media URLs are never exposed. See SECURITY.md.
Data license: the report data served by this API is dedicated to the public domain under CC0 1.0 β use it freely, no attribution required. This is separate from the source code's Apache 2.0 license.
See CONTRIBUTING.md for guidelines on how to submit issues, propose features, and open pull requests.
See SECURITY.md for our vulnerability disclosure policy.
Copyright 2026 Roiner Adrianza
Licensed under the Apache License, Version 2.0. See LICENSE for the full text.