4dim / Notas

Nunca conectes un canal real a un inquilino de demostración

Un simulador que sube las palomitas por reloj y un despachador real se pelean por la misma fila. Ninguno sabe del otro, y gane quien gane, el estado que se pinta no significa nada.

Una palomita que la pone un reloj

El 15 de agosto de 2026 íbamos a conectar el primer WhatsApp real de Connection. Antes de enchufarlo se leyó el código del inquilino que existía: tenía encendida la bandera is_demo, que arranca un simulador de entrega —sube los estados por reloj para que una demostración se entienda sin conectar nada—. Por eso el canal no se conectó ahí: se creó un inquilino nuevo para él.

Conviene decirlo ya: esto no es la crónica de un desastre. No hubo mensajes perdidos ni hay cifra de daño. Fue una trampa vista a tiempo. Lo que se cuenta es el mecanismo, leído línea a línea sobre el commit ea937e2 el 13 de septiembre de 2026; lo que no está medido va dicho así.

La pregunta que resuelve: si a Meta nunca le llegó una petición, ¿de dónde sale una doble palomita?

Las dos escaleras

Un mensaje saliente sube cuatro peldaños: encolado → enviado → recibido → leído. En Connection hay dos implementaciones que la suben: una con los acuses del webhook, otra con un reloj, en app/lib/entrega.mjs. La tabla cita literal la referencia de estados de la Cloud API (13-09-2026).

Los tres umbrales del reloj son constantes literales de ese archivo (líneas 22-26): dos segundos, seis y veinticinco. No son un horario. Son edades mínimas medidas contra created_at, no contra el peldaño anterior (línea 50), y el pase solo corre cuando alguien pinta la bandeja: si nadie mira durante treinta segundos, los tres pasos caen en la misma pasada y el mensaje va de encolado a leido de una vez. Entre recibido y leido, además, el ojo solo distingue un color: naranja quemado, no el azul de WhatsApp.

PeldañoQué significa según MetaQuién lo sube en un inquilino demoQuién lo sube de verdad
enviado / sent«successfully sent from our servers»El reloj, ≥ 2 s desde created_at, al pintar la bandejaEl despachador, cuando Meta devuelve el wamid
recibido / delivered«successfully delivered to the WhatsApp user's device»El reloj, ≥ 6 s desde created_atEl acuse delivered
leído / read«displayed in an open chat thread in the WhatsApp user's device»El reloj, ≥ 25 s desde created_atEl acuse read
fallido / failed«failure to send or deliver the message to the WhatsApp user's device»Nadie: el reloj no falla nuncaEl acuse failed o el error del envío
played«the first time a voice message is played by the WhatsApp user's device»No existeSolo del lado de Meta

La doble palomita no dice que el mensaje llegó. Dice que pasaron veinticinco segundos y que alguien abrió la bandeja.

La carrera por la misma fila

Las dos escaleras escriben en la misma columna, y ninguna de las dos consultas sabe que la otra existe.

CondiciónavanzarEntregaDemo()
entrega.mjs
despacharPendientes()
despacho.mjs
InquilinoAND t.is_demo (línea 47)Cualquiera: no mira la bandera nunca
BandejaNo filtrac.connection_id = $2
CredencialesNo las miracredentials IS NOT NULL y status = 'conectada' (53-54)
Estado de partidaLos tres primeros peldañosSolo 'encolado' (línea 111)
Edad del mensajecreated_at < now() - umbral (línea 50)Ninguna. Esta fila decide la carrera
FichajeNingunoclaimed_at, FOR UPDATE ... SKIP LOCKED
Tope por paseTodas las filas que cumplanTANDA = 5
Qué la disparaEl render de la bandeja (único llamador)Un POST del navegador y un temporizador de systemd (OnUnitActiveSec=60s)

La única condición de inquilino del reloj es AND t.is_demo: avanza todo mensaje saliente, sea sembrada o real la conversación.

De dónde sale esto: el 15 de agosto de 2026, en el commit que conecta el primer WhatsApp real, las dos funciones se llamaban seguidas en el mismo render de la bandeja, nueve líneas, el reloj primero. Duró un día —se separaron el 16, cuando el despacho pasó al navegador—. Hoy siguen sin conocerse.

Y el matiz que cambia el cuento: el reloj exige que el mensaje tenga más de dos segundos y el despachador no exige edad ninguna, así que en el camino interactivo el favorito es el despachador. El reloj gana cuando pasan más de dos segundos sin un pase: una tanda encolada de noche, nadie mirando. Esto es lectura de código, no medición: no está cronometrado, ni hay contador que diga con qué frecuencia gana cada camino.

Sin vuelta atrás

Lo grave no es que el reloj se adelante, sino que el mensaje que se lleva no se recupera nunca, por tres razones:

  1. El despachador solo reclama mensajes en 'encolado'. Uno que subió un peldaño le es invisible, y ninguna otra consulta del repositorio lo recoge.
  2. Los acuses se aplican por WHERE external_id = $1, el wamid que el despachador escribe al enviar. Ese mensaje lo tiene nulo: nunca salió.
  3. La escalera real solo sube, con una excepción que hay que decir: fallido entra desde cualquier peldaño.

El caso contrario es igual de feo, y va etiquetado como lo que es: una deducción de dos sentencias SQL, no una medición. Si gana el despachador, el mensaje sale; el reloj lo sube igual, y el acuse verdadero de delivered llega a una fila que ya está más arriba y se descarta en silencio.

La excepción tiene un lado útil: un fallo real sí se registra, con su código de Meta —190, token caducado; 131047, fuera de las 24 horas; 131026, imposible entregar—, comprobados el 13-09-2026. El mensaje que se llevó el reloj no produce ninguno: nunca hubo petición.

Y también miente sobre la ventana de 24 horas

Hay una segunda pieza que engaña igual. Un disparador de la base fija last_inbound_at —la hora del último mensaje del cliente— con cualquier entrante que no sea borrador, sin mirar si el inquilino es demo ni de dónde viene.

Esa columna es la que el despachador consulta para decidir si es legal mandar texto libre. Un cliente inventado abre una ventana de servicio de 24 horas idéntica a una real: el sistema cree que puede escribir libre, Meta lo rechaza y el mensaje no sale. La migración es del 27 de agosto de 2026, doce días después de lo del apartado 01: no es parte de aquello, es un riesgo de hoy.

Y hay un tercer camino, verificado pero no reproducido. Cuando a la función que abre un hilo no se le dice bandeja, elige la más antigua —ORDER BY id LIMIT 1—, y tres sitios crean un entrante sin decírselo. Si el canal real se conectó antes del primer «probar ahora», el cliente inventado entra por la bandeja de verdad.

La regla, y lo que sigue abierto

Un entorno de pruebas bien construido falla en voz alta y vive en otra entidad —otro número, otra cuenta—, no detrás de una bandera en la tabla de producción.

SistemaCómo separa pruebas de producciónSi mandas a alguien que no está de altaFuente
Twilio SandboxNúmero aparte: «Use the Twilio Sandbox for WhatsApp for testing and discovery purposes only. Don't use it in a production environment»«Messaging other users will fail with Error 63015». Un mensaje cada tres segundos, y la sesión caduca a los tres díastwilio.com/docs/whatsapp/sandbox, 13-09-2026
WhatsApp Cloud APINúmero de pruebas distinto: «a test business phone number is generated and registered for you automatically»No documentado en la página primaria; las cifras que circulan son de segunda manobusiness phone numbers, 13-09-2026
Connection, hoyUna bandera is_demo en la misma tabla de inquilinosEl mensaje se marca como leído y puede no salir nuncaentrega.mjs:22-52, commit ea937e2, 13-09-2026

Hasta Meta avisa de que su modo de desarrollo se come señales: «some webhooks will not be sent if your app is in Dev mode» (webhooks, 13-09-2026). Un entorno que calla es un problema conocido; uno que inventa la señal buena es peor. El componente que dibuja las palomitas lleva escrito el contrato que el reloj incumple: «Dar por leído lo que nadie confirmó es la clase de mentira que se paga con una venta».

El arreglo de agosto fue operativo, no estructural: un inquilino aparte, consolidar apagando is_demo, borrar el sobrante. A 13 de septiembre de 2026 el código sigue igual. entrega.mjs no se toca desde el 12 de agosto; conectar un WhatsApp comprueba el token contra Meta pero no is_demo; y de los diez ficheros de prueba de app/lib/, ninguno importa entrega.mjs ni despacho.mjs.

Lo que no se sabe:

  • Si el fallo llegó a ocurrir. No: hay constancia escrita de que se evitó.
  • Si hubo una ventana el 16 de agosto. No consta en qué orden se movió la conexión y se apagó la bandera.
  • Cuántos mensajes se perdieron. Cero dato: lo que no salió no deja rastro del lado de Meta.
  • Cuánto dura la carrera. Las constantes son exactas; quién llega primero, sin cronometrar.
  • Si otro inquilino ha tenido la bandera encendida con un canal real. No se ha consultado producción.
  • Si otros CRM tienen el mismo patrón. No se ha auditado ninguno.

Tres guardas lo cerrarían: que el reloj exija que la conexión no tenga credenciales; que conectar un canal real sobre un inquilino demo esté prohibido, o al menos avise; y una prueba que enfrente las dos consultas sobre una misma fila.

Para quien sospeche de su bandeja, el criterio no es contar peldaños: Meta documenta que a veces se salta delivered —«the message is both delivered and read at the same time. In these cases, the 'delivered' webhook is not sent»—, y una escalera completa es lo normal. Lo que delata a un reloj es la regularidad: si todos los mensajes suben con los mismos intervalos, ninguno se salta un peldaño y no falla nunca ninguno, eso no lo sube Meta.

Esta nota sale de nuestro trabajo en AI-CRM Connection.

← todas las notas