Tots els productes que hem llançat a Maladeta Studio acaben necessitant parlar amb persones fora d'una pestanya del navegador. Lueira envia respostes per WhatsApp. La plataforma de marketplace envia confirmacions de comanda i actualitzacions d'enviament per email. Les eines internes necessiten avisar algú quan un job falla o se supera un llindar. Cadascuna d'aquestes coses és una funcionalitat petita per si sola — fins que l'has construïda tres o quatre vegades i t'adones que estàs resolent els mateixos problemes una vegada i una altra: reintents quan un proveïdor falla, rate limits perquè no et marquin el domini d'enviament, idempotència perquè un reintent de webhook no enviï el mateix missatge dues vegades, i un registre d'auditoria perquè algú acabarà preguntant "això ho vam arribar a enviar de debò?".
A principis d'enguany vam deixar de resoldre això per producte i vam construir Maladeta Communications: una plataforma independent, multi-tenant i basada en esdeveniments el sol treball de la qual és rebre esdeveniments i convertir-los en missatges lliurats, per WhatsApp o email, amb generació de contingut assistida per IA opcional. Aquest post tracta de per què la vam construir com un servei propi, com està muntada, i un límit que vam tenir cura de traçar-hi al voltant.
1. Per què cada producte resolent això pel seu compte no escala
Aquest és el mateix argument que recorre altres posts d'aquest blog — el de la facturació electrònica, el de les signatures eIDAS. La complexitat que de debò es comparteix entre productes val la pena absorbir-la un cop, en un sol lloc, en lloc de resoldre-la de nou per equip, per repositori, per data límit. El lliurament de missatges té exactament aquesta forma. Resend i WhatsApp (a través del nostre proveïdor, Kapso) tenen cadascun les seves pròpies particularitats, rate limits i modes de fallada. Encertar amb els reintents i el backoff és prou delicat com per no voler tres implementacions diferents desincronitzant-se entre elles. I tant compliance com suport acaben demanant, tard o d'hora, un registre durador de què es va enviar, a qui, i quan.
Així que en lloc que cada producte cridi directament Resend o Kapso, publica un esdeveniment. El motor de comunicacions decideix què passa després.
2. Entren esdeveniments, surten missatges
La forma del sistema és un pipeline directe, deliberadament avorrit:
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)
Les fonts externes — un webhook de Kapso, el nostre proveïdor de WhatsApp, i un stream d'esdeveniments que ve de la mateixa Lueira — aterren en un ConnectorRegistry, que els normalitza i els passa a un EventPublisher. Aquest publisher escriu en dos llocs: PostgreSQL, com a registre d'auditoria permanent i consultable — tot esdeveniment que hagi entrat mai al sistema, registrat de manera duradora, sense excepcions — i un Redis Stream, per al processament en temps real.
Un consumer llegeix aquest stream fent servir XREADGROUP, la primitiva de Redis per a un repartiment fiable, almenys un cop, cap a un grup de workers. Reparteix els esdeveniments en tres cues de BullMQ separades — email, whatsapp i ai — cadascuna recolzada pel seu propi worker. EmailWorker parla amb Resend. WhatsAppWorker parla amb Kapso. AIWorker gestiona l'encaminament i la generació de contingut per als casos en què un missatge s'ha de redactar en lloc de simplement retransmetre's. Separar les cues significa que un proveïdor de WhatsApp lent o caigut mai bloqueja el lliurament d'email.
3. Un límit deliberat: lliurament, no raonament
Si has llegit els posts anteriors d'aquest blog sobre l'agent d'IA de WhatsApp de Lueira i la seva configuració de function-calling, sabràs que el RAG, la construcció de prompts, les crides al model i els embeddings ja viuen en algun lloc — al repositori de l'assistent de Lueira, amb la seva pròpia instància de Qdrant i la seva pròpia orquestració basada en Mastra. Hauria estat fàcil, en construir un servei nou que també toca WhatsApp i també toca Claude, deixar que part d'aquell raonament es colés de nou aquí. Vam ser explícits que no havia de passar. La guia de contribució d'aquest repositori diu, literalment: "Els agents d'IA, els prompts, les crides al model, els embeddings d'articles, Qdrant i la transcripció pertanyen al repositori de l'assistent de Lueira. No reintrodueixis aquí un servei d'IA local ni una dependència directa de l'SDK de Mastra/Qdrant".
El que això vol dir a la pràctica: quan l'AIWorker necessita capacitat d'IA real — respondre a un missatge, generar contingut, transcriure àudio — crida l'assistant-service independent de Lueira per HTTP, contra endpoints com /ai/reply, /ai/generate i /ai/transcribe. Aquest motor decideix si i com encaminar i enviar un missatge. És infraestructura de lliurament, no un segon lloc on passa el raonament.
4. Escalar les peces per separat
Com que la capa d'API/webhook, els workers de les cues i el consumer del stream fan feines genuïnament diferents, no necessiten escalar junts. Una variable d'entorn PROCESS_TYPE — all, api, worker o consumer — controla el que realment executa una instància de procés donada. En producció, podem aixecar més processos worker durant un pic d'enviaments de WhatsApp sense tocar la capacitat de l'API, o escalar el consumer si el Redis Stream comença a acumular retard.
Aquesta és una peça d'infraestructura genuïnament nova — la vam construir el 2026, i no ha tingut anys per curtir-se en producció com alguns dels nostres altres sistemes. Però la seva forma ja se sent correcta: un sol lloc que absorbeix les parts molestes i compartides d'enviar un missatge de manera fiable, i una línia ferma que el manté fora del negoci de decidir què hauria de dir aquest missatge.