Cuando tu interfaz en la nube necesita imprimir una etiqueta física en una impresora USB que está en un almacén, hace falta una solución arquitectónica creativa. Este es el patrón que construí.
El problema
El frontend de mi ERP a medida se ejecuta en Retool Cloud. Mi impresora de etiquetas (Brother QL-700) está conectada por USB a un PC con Windows en el almacén. Mi base de datos (NeonDB) está en la nube. Cuando un empleado pulsa «Imprimir etiqueta» en Retool, tiene que salir una etiqueta física de una impresora situada a 3.000 km de los servidores de Retool.
Retool Cloud no puede comunicarse con una impresora local. No hay una ruta de red directa. El reenvío de puertos es frágil. Las VPN añaden complejidad. Los webhooks requieren un endpoint público.
Necesitaba algo más sencillo y fiable.
La solución: PostgreSQL como cola de comandos
La clave estaba en que ambos lados —Retool Cloud y el backend local— ya comparten una cosa: la base de datos PostgreSQL. ¿Por qué no usarla como intermediario de mensajes?
Retool Cloud NeonDB (PostgreSQL)
│ │
│ INSERT INTO │
│ extraction_command_queue │
│ (type='print', payload=...) │
│ ──────────────────────────────────→ │
│ │
│ │ ← POLL every 5s
│ │ (SELECT ... WHERE
│ │ status='pending')
│ │
│ Local Backend (Windows)
│ │
│ │ Process command
│ │ Download label PDF
│ │ Send to Brother QL-700
│ │ UPDATE status='completed'
│ │
│ ← Query result │
│ (status='completed') │
Los detalles de ingeniería
Bloqueo exclusivo
¿Y si se están ejecutando dos instancias del backend? ¿O si el backend se cae a mitad de un comando? Implementé un patrón de bloqueo exclusivo:
- Al arrancar, el
CommandPolleradquiere un bloqueo escribiendo su nombre de host y su PID en una tablabackend_lock - Antes de procesar cualquier comando, comprueba que sigue teniendo el bloqueo
- Si otra instancia intenta arrancar, ve el bloqueo y se retira
Monitorización mediante latido
Un bloqueo sin latido es un interbloqueo en potencia. Cada 30 segundos, el backend activo actualiza su marca de tiempo last_heartbeat. Si el latido está obsoleto (>60 segundos), cualquier instancia nueva del backend puede robar el bloqueo, partiendo de la suposición de que el anterior propietario se ha caído.
Tipos de comando
La cola gestiona algo más que la impresión:
- Imprimir etiqueta: descargar la etiqueta de transporte desde la API de Wallapop, generar el código de barras y enviarlo a la Brother QL-700
- Imprimir etiqueta LPN: generar un código de barras Code128 para un artículo de inventario e imprimirlo en una etiqueta térmica
- Enriquecer artículo: activar el pipeline de enriquecimiento con IA (scraping de Amazon → categorizar → generar descripción)
- Ejecutar extracción: lanzar una extracción bajo demanda de pedidos, chats o anuncios
Recuperación ante fallos
Los comandos pueden fallar (impresora desconectada, Amazon bloqueando el scraping, tiempo de espera de red agotado). El sistema lo gestiona:
- Los comandos fallidos se marcan con
status='failed'y un mensaje de error - Retool muestra el error al usuario con un botón «Reintentar»
- Al arrancar, el backend busca comandos huérfanos (status='processing' pero con el latido obsoleto) y los restablece a 'pending'
Por qué esto funciona mejor que las alternativas
- Frente a los webhooks: no hace falta un endpoint público. Ni certificados SSL. Ni DDNS. El backend local es simplemente un consumidor que sondea una base de datos
- Frente a las colas de mensajes (RabbitMQ, Redis): no hay infraestructura adicional. PostgreSQL ya está ahí. Para mi volumen (~50 comandos al día) es perfecto
- Frente a WebSocket: no hay que mantener una conexión persistente. Sondear cada 5 segundos es suficiente para imprimir etiquetas: un retardo de 5 segundos es imperceptible
Resultados en producción
Este sistema lleva más de un año funcionando en producción. Procesa de forma fiable comandos de impresión, solicitudes de enriquecimiento y disparadores de extracción. La conmutación por error se ha puesto a prueba (sin querer) varias veces cuando mi PC local se ha reiniciado: el backend arranca de nuevo, recupera el bloqueo y retoma el procesamiento en menos de 60 segundos.
El patrón es de código abierto: github.com/AspiranteD/cloud-local-command-bridge