Telemetría pública¶
qrsgen incluye un endpoint opt-in pensado para que una landing estática (GitHub Pages, Cloudflare Pages, Netlify…) muestre contadores en vivo: instancias conectadas, totales de mensajes, etc.
Endpoint¶
Sin auth. Cuando PUBLIC_STATS_ENABLED=false (default) devuelve 403.
Response:
{
"instances_connected": 4,
"instances_total": 4,
"installations_active": 4,
"installations_total": 37,
"tenants_total": 2,
"qrs_scanned_total": 42,
"messages_in_total": 152340,
"messages_out_total": 178921,
"version": "0.28.2",
"last_updated": "2026-05-27T13:00:00Z"
}
Cache 30s in-memory (desde v0.24.2): la landing hace polling cada
10s; el primer hit dentro de cada ventana de 30s reconstruye el payload
desde DB, los siguientes ~2 lo sirven cacheados. last_updated refleja
el momento en que se computó el payload, no la request actual — un
desfase de hasta 30s es esperado y aceptable para una landing.
Las parejas son live vs histórico:
instances_connected/instances_total— snapshot live: lo que el proceso tiene en memoria ahora mismo.installations_active/installations_total— datos persistidos: lo que ha existido en DB en cualquier momento.
Campos:
| Campo | Significado |
|---|---|
instances_connected |
Cuántas instancias están ahora mismo en estado ready o connected (snapshot live). |
instances_total |
Total de instancias registradas en el proceso ahora mismo (incluye qr_pending, disconnected, etc.). Snapshot live. |
installations_active |
Instancias en DB con jid configurado — han llegado a parearse al menos una vez y siguen registradas. |
installations_total |
Histórico acumulado de instancias que han existido en algún momento — incluye las ya borradas via DELETE (sobrevive a borrar porque cuenta entradas distintas en bridge_audit_log, que es append-only). |
tenants_total |
Filas en bridge_tenant ahora mismo (cuántos clientes multi-downstream tienen config propia). Desde v0.24.1. |
qrs_scanned_total |
Histórico acumulado de pairings exitosos (= veces que un usuario ha escaneado un QR). Cuenta filas en bridge_audit_log con action='instance.paired'. Incluye re-pairings de una misma instancia. |
messages_in_total / messages_out_total |
All-time totals agregados desde bridge_usage_daily. |
version |
Tag de release del binario. |
last_updated |
Timestamp UTC de la respuesta. |
Si quieres ventanas concretas (último mes, último día, etc.) usa los
endpoints autenticados /api/usage o /api/usage/summary.
Habilitar¶
En el .env del stack:
PUBLIC_STATS_ALLOW_ORIGIN controla el header CORS. Pon el origen exacto
de tu landing (no uses * — limita la exposición). Si lo dejas vacío,
el endpoint funciona pero el navegador bloquea las requests cross-origin.
Exponer a internet via Traefik¶
Si el VPS ya tiene Traefik (lo más común), añade labels al servicio qrsgen en el compose:
services:
qrsgen:
image: qrsgen:${QRSGEN_VERSION}
# ... (resto de la config)
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.qrsgen-public.rule=Host(`telemetry.qrsgen.ricajos.dev`) && Path(`/api/public/stats`)"
- "traefik.http.routers.qrsgen-public.entrypoints=websecure"
- "traefik.http.routers.qrsgen-public.tls=true"
- "traefik.http.routers.qrsgen-public.tls.certresolver=letsencrypt"
- "traefik.http.services.qrsgen-public.loadbalancer.server.port=3100"
# Rate limit defensivo (10 req/s, burst 20).
- "traefik.http.middlewares.qrsgen-ratelimit.ratelimit.average=10"
- "traefik.http.middlewares.qrsgen-ratelimit.ratelimit.burst=20"
- "traefik.http.routers.qrsgen-public.middlewares=qrsgen-ratelimit"
Apuntas el DNS telemetry.qrsgen.ricajos.dev a la IP del VPS y Traefik
hace lo demás (certbot Let's Encrypt + path routing). El resto de la API
qrsgen sigue solo accesible desde la overlay LAN — Traefik solo expone
ese path concreto.
Sin Traefik¶
Si usas otro reverse proxy (nginx, Caddy, etc.), el principio es el mismo:
- Solo enrutas el path
/api/public/statsal backendqrsgen:3100. - TLS terminado en el proxy.
- Rate limit defensivo (10 req/s suficiente).
- CORS lo emite qrsgen mismo via
PUBLIC_STATS_ALLOW_ORIGIN— el proxy no necesita tocarlo.
Frontend en la landing¶
La landing de qrsgen incluye un widget JS que hace polling cada 10s al
endpoint y actualiza cuatro cards (QRs conectados, instalaciones totales,
mensajes in/out totales). Configurable via data-endpoint en el bloque
HTML — ver
docs/index.md
para el snippet.
El JS guarda en localStorage si el visitante quiere ver la telemetría
o no — por defecto OFF (opt-in del cliente). Un botón "Activar / Pausar"
controla el polling.
Verificación¶
Tras habilitar y exponer:
# Desde la LAN (debe funcionar):
curl -sS http://qrsgen:3100/api/public/stats
# Desde internet (debe funcionar también):
curl -sS https://telemetry.qrsgen.ricajos.dev/api/public/stats
# CORS preflight:
curl -i -X OPTIONS https://telemetry.qrsgen.ricajos.dev/api/public/stats \
-H "Origin: https://rricajos.github.io"
# Debe devolver Access-Control-Allow-Origin: https://rricajos.github.io
# Cuando PUBLIC_STATS_ENABLED=false:
curl -sS http://qrsgen:3100/api/public/stats
# {"error":"public stats disabled"} HTTP 403
Consideraciones de privacidad y seguridad¶
- El endpoint expone solo contadores agregados. Ningún JID, número de teléfono, contenido de mensaje ni metadato individual.
- Un atacante puede observar el ritmo de actividad (delta de
messages_*_totalentre polls). Si esto es información sensible para tu caso de uso, no actives el endpoint público. - Apaga
PUBLIC_STATS_ENABLEDinstantáneamente para revocar la exposición: el siguiente request al endpoint devuelve 403. - Rate limit en Traefik (10 req/s) evita scraping abusivo. El payload es ~200 bytes — barato pero no infinito.
Glosario¶
Endpoint público: HTTP endpoint accesible desde internet (no solo desde la overlay LAN). qrsgen tiene uno opt-in para telemetría.
Opt-in: feature desactivada por default. Hay que activarla explícitamente (env var = true).
CORS (Cross-Origin Resource Sharing): mecanismo HTTP que permite a
una página web hacer requests a otro dominio. qrsgen emite el header
Access-Control-Allow-Origin cuando se le configura.
Origin: el scheme://host:port desde donde se sirve una página web.
El navegador lo manda en cada request cross-origin para que el servidor
decida si autorizarla.
Preflight (CORS): petición OPTIONS que el navegador hace antes de una request cross-origin para preguntar si el servidor la acepta. qrsgen responde con headers Access-Control-* apropiados.
Reverse proxy: servidor (Traefik, nginx, Caddy) que recibe requests externas y las enruta a backends internos. Termina TLS y aplica middlewares.
Traefik labels: anotaciones en el compose que Traefik lee automáticamente para descubrir qué containers exponer y bajo qué reglas.
certresolver: configuración de Traefik que pide certificados TLS a Let's Encrypt automáticamente para cada nuevo Host.
Host rule: regla de routing de Traefik que matchea por hostname
(Host(\telemetry.qrsgen.ricajos.dev`)`).
Path rule: regla que matchea por path URL
(Path(\/api/public/stats`)`). qrsgen lo combina con la Host rule
para exponer solo ese endpoint, no toda la API.
Rate limit (Traefik middleware): middleware que limita requests por segundo por cliente. Defensa contra scraping y DDoS ligeros.