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:
docs/openapi.yaml— especificación de ambas superficiesinternal/api/middleware/middleware.go— cadena de ABMinternal/consumers/middleware/— cadena de ingestainternal/routes/url_mappings.go— registro de rutas
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.