Saltar a contenido

Contenedores (C4 nivel 2)

Responde: ¿qué procesos y almacenes hay, dónde corre cada uno y cómo se hablan?

Cada flecha indica su protocolo y, cuando existe, su mecanismo de autenticación.

flowchart TB
  B["🌐 Navegador"]

  subgraph vercel["Vercel"]
    FE["<b>Frontend + BFF</b><br/>Next.js 16 · React 19<br/><i>35 páginas · 62 rutas BFF</i>"]
  end

  subgraph nube["Nube"]
    API["<b>Cloud API</b><br/>Go 1.24 · Gin<br/><i>Cloud Run</i>"]
    PG[("<b>Postgres</b><br/>identidad, RBAC,<br/>dispositivos, claves,<br/>layouts, logs")]
    MG[("<b>MongoDB</b><br/>mediciones")]
    RD[("<b>Redis</b><br/>rate limit<br/>idempotencia")]
    SB["<b>Supabase</b><br/>Auth · JWKS · correo"]
  end

  subgraph pi["Raspberry Pi — planta"]
    HI["<b>Historian</b><br/>Python · Modbus"]
    IF[("<b>InfluxDB</b><br/>historian_semantic")]
    FA["<b>FA³ST</b><br/>servidor AAS<br/><i>override opcional</i>"]
    ED["<b>Edge Pi Service</b><br/>Go · Gin"]
    SQ[("<b>SQLite</b><br/>outbox + watermark")]
  end

  PLC["⚙️ PLC"]

  B -->|"HTTPS · cookie de sesión"| FE
  B -->|"HTTPS · login"| SB
  FE -->|"HTTP(S) · Bearer JWT + X-Tenant-ID"| API

  API -->|"pgx · DATABASE_URL"| PG
  API -->|"driver v2 · MONGO_URI"| MG
  API -->|"go-redis · REDIS_URL"| RD
  API -->|"HTTPS · JWKS + Admin API"| SB
  API -->|"HTTP(S) · GET /status /health /telemetry<br/><b>sin autenticación</b>"| ED

  ED -->|"HTTP(S) · X-Api-Key<br/>+ Idempotency-Key"| API
  ED -->|"HTTP · Flux + INFLUX_TOKEN"| IF
  ED -->|"archivo local"| SQ

  HI -->|"Modbus TCP · solo lectura"| PLC
  HI -->|"HTTP · escritura + INFLUX_TOKEN"| IF
  HI -.->|"archivo XML compartido"| FA

  classDef app fill:#1168bd,stroke:#0b4884,color:#fff
  classDef store fill:#438dd5,stroke:#2e6295,color:#fff
  classDef ext fill:#999,stroke:#6b6b6b,color:#fff
  classDef opt fill:#1168bd,stroke:#0b4884,color:#fff,stroke-dasharray: 5 5
  class FE,API,ED,HI app
  class FA opt
  class PG,MG,RD,IF,SQ store
  class PLC,SB,B ext

Sobre HTTP(S)

Las tres flechas marcadas HTTP(S) tienen la URL base configurable (BACKEND_URL en el frontend, CLOUD_BASE_URL en el Edge, raspberryBaseUrl por dispositivo en el Cloud). En producción se usan endpoints HTTPS, pero en el empaquetado local del bundle son HTTP plano. El esquema depende del despliegue, no del código.

Contenedores

En la nube

Contenedor Tecnología Responsabilidad
Frontend + BFF Next.js 16, React 19, Vercel Interfaz de usuario y capa BFF. El navegador nunca llama directo a la API: el BFF inyecta el Authorization: Bearer y resuelve el X-Tenant-ID
Cloud API Go 1.24, Gin, Cloud Run Núcleo de negocio. Expone dos superficies con modelos de autenticación distintos
Postgres Supabase Identidad, RBAC, tenants, dispositivos, claves de API, layouts, logs, reglas de alarma, notificaciones
MongoDB driver v2 Mediciones. Índice único (tenantId, eventId) para deduplicar
Redis go-redis Rate limiting e idempotencia. Fail-open: si no responde, la API sigue sin aplicar límites
Supabase Auth SaaS Emisión y firma de JWT, JWKS, invitaciones y recuperación de contraseña

En la Raspberry Pi

Contenedor Tecnología Responsabilidad
Historian Python, Modbus TCP Lee el PLC, aplica el mapeo semántico AAS, escribe en InfluxDB y actualiza el modelo AAS. Tesis de maestría
InfluxDB 2.7 en el bundle · 2.6 en el compose propio del historian Serie temporal local. Measurement historian_semantic con tags derivados del AAS. El bucket no tiene retención configurada: los datos no expiran
FA³ST Eclipse FA³ST Sirve el modelo AAS. Override opcional, no parte del despliegue base
Edge Pi Service Go, Gin Lee InfluxDB, encola en el outbox y reenvía al Cloud. Expone endpoints de estado hacia el Cloud
SQLite — Outbox de eventos pendientes y watermark de lectura

FA³ST no forma parte del despliegue base

El servidor AAS vive en docker-compose.aas.yml, un override que hay que activar explícitamente; el docker-compose.yml base del bundle no lo levanta. Cuando se activa, se publica por HTTPS con certificado autofirmado y expone la API /api/v3.1/.

Hoy no tiene consumidores dentro del sistema.

Las dos superficies de la Cloud API

Es la decisión estructural más importante del backend: dos superficies HTTP con cadenas de middleware completamente distintas.

ABM Ingesta
Consumidor Frontend (BFF) Edge Pi Service
Autenticación JWT verificado contra JWKS, con solo ES256 y RS256 admitidos Clave de API por dispositivo
Tenant Por header Resuelto del lado del servidor a partir de la clave
CORS Habilitado Deshabilitado explícitamente
Formato de respuesta Con envelope Contrato congelado, sin envelope

Los paths exactos, los nombres de header, el orden de la cadena de middlewares y el esquema de respuesta pertenecen al repositorio del Cloud y no se replican acá. Son el contrato, y su fuente de verdad es:

La explicación de la semántica de cada frontera está en integraciones.

Componentes internos del Edge

El Edge es el contenedor con la lógica más delicada —evitar que se pierdan o dupliquen datos ante cortes de enlace—.

Paquete Rol
scheduler Dispara el ciclo de reenvío periódicamente
platform/influxclient Lee puntos nuevos desde el watermark, con un margen de seguridad
app/forwarder Agrupa los puntos en eventos y arma los lotes
repo/sqlite Persiste el outbox y el watermark
platform/cloudclient Envía al Cloud con backoff exponencial y respeta Retry-After
app/checks, platform/plcprobe, platform/sysprobe Alimentan los endpoints de estado
app/catalog Resuelve el aasPath de cada punto desde el mapeo AAS
app/heartbeat Vacío: solo contiene un doc.go. La funcionalidad no está implementada

Capacidad del outbox

Una decisión del Edge plantea como contexto un buffer offline de 24 h o más. La implementación no lo garantiza. Lo que el código impone es un límite de filas, OUTBOX_MAX_ROWS, con valor por defecto 100000, que el bundle no modifica: la guía de despliegue en la Pi estima que eso equivale a unas 1,5 h de backlog a ~18 eventos/s.

Al llenarse el outbox, el Edge deja de aceptar eventos nuevos pero no pierde datos: el watermark solo avanza sobre los eventos efectivamente aceptados, así que el mecanismo es de contrapresión, no de descarte.

Alcanzar 24 h reales requiere subir OUTBOX_MAX_ROWS y validar el espacio en disco de la Pi.

Degradación

Qué deja de funcionar cuando cae cada pieza:

Cae… Consecuencia
Redis Nada deja de funcionar. Se pierde el rate limiting (fail-open)
MongoDB La ingesta responde 500 y los dashboards se quedan sin datos. Login y ABM siguen funcionando
Supabase / JWKS La API devuelve 503: nadie puede autenticarse
Postgres Cae todo el ABM
Enlace planta ↔ nube El Edge sigue capturando en el outbox hasta OUTBOX_MAX_ROWS y reanuda al volver. Superado el límite, aplica contrapresión sin perder datos
Historian o InfluxDB No hay datos nuevos. El Edge arranca en modo scaffold: responde los endpoints de estado con el forwarder deshabilitado
FA³ST Nada: es un override opcional y hoy no tiene consumidores

Dos huecos del dibujo

Los chequeos del Cloud hacia la Pi van sin credenciales. El Edge admite un secreto de cabecera opcional y el Cloud no lo envía, así que el contrato inbound apoya en una red privada que no está configurada en ningún repositorio. El modelo completo y el riesgo asumido están en seguridad.

El heartbeat no existe en ninguno de los dos extremos. El Cloud responde 501 y el paquete correspondiente del Edge está vacío, así que el estado de un dispositivo se obtiene solo por los chequeos que inicia el Cloud: una Pi que deja de enviar no avisa.

Tres cosas del despliegue no están en ningún repositorio y hay que mirarlas en las consolas: la topología de red entre el Cloud y la planta, el proveedor de MongoDB en producción, y quién recolecta las métricas Prometheus que exponen el Cloud y el Edge.

Siguiente nivel

El detalle de cada frontera —semántica, errores, secuencias— está en integraciones, y el recorrido completo de una variable en flujo de datos.