Todos los productos que hemos lanzado en Maladeta Studio acaban necesitando hablar con personas fuera de una pestaña del navegador. Lueira envía respuestas por WhatsApp. La plataforma de marketplace envía confirmaciones de pedido y actualizaciones de envío por email. Las herramientas internas necesitan avisar a alguien cuando un job falla o se cruza un umbral. Cada una de esas cosas es una funcionalidad pequeña por sí sola — hasta que la has construido tres o cuatro veces y te das cuenta de que estás resolviendo los mismos problemas una y otra vez: reintentos cuando un proveedor falla, rate limits para que no te marquen el dominio de envío, idempotencia para que un reintento de webhook no envíe el mismo mensaje dos veces, y un registro de auditoría porque alguien acabará preguntando "¿esto lo llegamos a enviar de verdad?".
A principios de este año dejamos de resolver eso por producto y construimos Maladeta Communications: una plataforma independiente, multi-tenant y basada en eventos cuyo único trabajo es recibir eventos y convertirlos en mensajes entregados, por WhatsApp o email, con generación de contenido asistida por IA opcional. Este post trata de por qué la construimos como un servicio propio, cómo está montada, y un límite que tuvimos cuidado de trazar a su alrededor.
1. Por qué cada producto resolviendo esto por su cuenta no escala
Este es el mismo argumento que recorre otros posts de este blog — el de la facturación electrónica, el de las firmas eIDAS. La complejidad que de verdad se comparte entre productos merece absorberse una vez, en un solo sitio, en lugar de resolverse de nuevo por equipo, por repositorio, por fecha límite. La entrega de mensajes tiene exactamente esa forma. Resend y WhatsApp (a través de nuestro proveedor, Kapso) tienen cada uno sus propias particularidades, rate limits y modos de fallo. Acertar con los reintentos y el backoff es lo bastante delicado como para no querer tres implementaciones distintas de eso desincronizándose entre sí. Y tanto compliance como soporte acaban pidiendo, tarde o temprano, un registro duradero de qué se envió, a quién, y cuándo.
Así que en lugar de que cada producto llame directamente a Resend o Kapso, publica un evento. El motor de comunicaciones decide qué pasa después.
2. Entran eventos, salen mensajes
La forma del sistema es un pipeline directo, deliberadamente aburrido:
Sources (Kapso webhook, Lueira event stream)
|
v
ConnectorRegistry
|
v
EventPublisher
|
+----+----+
| |
v v
Postgres Redis Stream
(audit) (real-time)
|
v
Consumer (XREADGROUP, consumer groups)
|
+----------+----------+
| | |
v v v
email Q whatsapp Q ai Q
| | |
v v v
EmailWorker WhatsAppWorker AIWorker
(Resend) (Kapso) (Claude routing/content)
Las fuentes externas — un webhook de Kapso, nuestro proveedor de WhatsApp, y un stream de eventos que viene de la propia Lueira — aterrizan en un ConnectorRegistry, que los normaliza y se los pasa a un EventPublisher. Ese publisher escribe en dos sitios: PostgreSQL, como registro de auditoría permanente y consultable — todo evento que haya entrado alguna vez en el sistema, registrado de forma duradera, sin excepciones — y un Redis Stream, para el procesamiento en tiempo real.
Un consumer lee ese stream usando XREADGROUP, la primitiva de Redis para un reparto fiable, al menos una vez, hacia un grupo de workers. Reparte los eventos en tres colas de BullMQ separadas — email, whatsapp y ai — cada una respaldada por su propio worker. EmailWorker habla con Resend. WhatsAppWorker habla con Kapso. AIWorker gestiona el enrutado y la generación de contenido para los casos en que un mensaje necesita ser redactado en lugar de simplemente retransmitido. Separar las colas significa que un proveedor de WhatsApp lento o caído nunca bloquea la entrega de email.
3. Un límite deliberado: entrega, no razonamiento
Si has leído los posts anteriores de este blog sobre el agente de IA de WhatsApp de Lueira y su configuración de function-calling, sabrás que el RAG, la construcción de prompts, las llamadas al modelo y los embeddings ya viven en algún sitio — en el repositorio del asistente de Lueira, con su propia instancia de Qdrant y su propia orquestación basada en Mastra. Habría sido fácil, al construir un servicio nuevo que también toca WhatsApp y también toca Claude, dejar que parte de ese razonamiento se colara de vuelta aquí. Fuimos explícitos en que no debía pasar. La guía de contribución de este repositorio dice, literalmente: "Los agentes de IA, los prompts, las llamadas al modelo, los embeddings de artículos, Qdrant y la transcripción pertenecen al repositorio del asistente de Lueira. No reintroduzcas aquí un servicio de IA local ni una dependencia directa del SDK de Mastra/Qdrant".
Lo que eso significa en la práctica: cuando el AIWorker necesita capacidad de IA real — responder a un mensaje, generar contenido, transcribir audio — llama al assistant-service independiente de Lueira por HTTP, contra endpoints como /ai/reply, /ai/generate y /ai/transcribe. Este motor decide si y cómo enrutar y enviar un mensaje. Es infraestructura de entrega, no un segundo lugar donde ocurre el razonamiento.
4. Escalar las piezas por separado
Como la capa de API/webhook, los workers de las colas y el consumer del stream hacen trabajos genuinamente distintos, no necesitan escalar juntos. Una variable de entorno PROCESS_TYPE — all, api, worker o consumer — controla lo que realmente ejecuta una instancia de proceso dada. En producción, podemos levantar más procesos worker durante un pico de envíos de WhatsApp sin tocar la capacidad de la API, o escalar el consumer si el Redis Stream empieza a acumular retraso.
Esta es una pieza de infraestructura genuinamente nueva — la construimos en 2026, y no ha tenido años para curtirse en producción como algunos de nuestros otros sistemas. Pero su forma ya se siente correcta: un solo sitio que absorbe las partes molestas y compartidas de enviar un mensaje de forma fiable, y una línea firme que lo mantiene fuera del negocio de decidir qué debería decir ese mensaje.