Propuesta · agosto 2026

Serie aurora · producto

La plataforma estrena versión. Mogos estrena oído

Un flotante vestido con la marca donde el cliente escribe una sugerencia como quien manda un mensaje. En el fondo se crea la sugerencia y llega a un canal de Slack de crescō con el detalle y un botón a su WhatsApp. Y del lado del cliente, lo que de verdad importa: una línea que dice «Vista por el equipo».

Pieza · flotante de sugerencias Superficie · apps/client Del otro lado · Slack de crescō Estado · propuesta

Lo que hay hoy

01 · el inventario

Mogos ya tiene sugerencias: una ruta, un formulario con calificación y comentario, una tabla, un módulo de API y hasta una pantalla de admin para leerlas. Lo que no tiene es el circuito. Tres hallazgos leídos en el código antes de dibujar nada.

01 La sugerencia pierde a su autor

page.tsx:42 monta <SuggestionForm /> sin props, y el API lee userId del body — que el formulario nunca manda.

Resultado: toda sugerencia web se guarda anónima, aun con sesión abierta. El admin las lista como «Anónimo».

02 No hay Slack en ninguna parte

Cero dependencia @slack/*, cero SLACK_* en los .env, cero webhooks en todo el repo.

SuggestionsService.create() es un prisma.create pelado: no avisa por ningún canal. El módulo ni siquiera importa notificaciones.

03 El estado que existe no es «visto»

SuggestionStatusEnum es PENDING · REVIEWED · IMPLEMENTED · REJECTED: un estado interno de gestión.

No hay seenAt, ni quién la vio, ni cuándo, ni dónde guardar una respuesta. «Vista» hay que inventarla.

La tesis. El formulario que existe es un buzón: recibe y calla. Lo que falta no es otra forma de escribir — es el viaje de vuelta. Esta pieza construye el circuito completo: una esquina donde escribir sin salir de lo que estabas haciendo, un canal donde el equipo lo lee de verdad, y una línea que le dice al cliente que lo leyeron. El formulario de /suggestions se queda donde está; esta es la puerta rápida, no su reemplazo.

La esquina que escucha

02 · el botón

56 px, superficie noche, el monograma en blanco con su punto rojo fijo. Vive en la esquina inferior derecha de toda la app del cliente y no compite con nada: el FAB de crear vive en el sidebar en escritorio y dentro de la barra de pestañas en móvil.

Escena · el home con la esquina La única forma de juzgar un flotante es verlo encima de algo. Debajo, el home retintado de aurora-home, sin cambios — y con la holgura inferior que le hace falta. nuevo
Los tres estados · a tamaño real La regla propia de esta pieza vive acá: en reposo el botón no se mueve.

ReposoQuieto. Sin halo, sin pulso, sin insignia. No pide nada.

1

Hay novedadTe respondieron, o algo que pediste ya está listo. Halo celeste de 3,2 s y la cifra: el único movimiento que existe.

Cuéntanos algo

Al pasar el ratónLa etiqueta dice para qué sirve. En móvil no existe: ahí el ícono se aprende tocándolo.

Por qué no late. Un halo permanente es un golpecito en el hombro cada tres segundos durante toda la sesión: enseña a ignorarlo, y cuando de verdad haya algo que ver, ya nadie lo mira. Acá el movimiento se guarda para el único momento en que significa algo. La insignia va fuera de la marca — el punto rojo del monograma es la firma de Mogos, nunca un contador.

Y flota, pero no tapa. Dibujar la escena lo dejó a la vista: puesto sin más, el botón se sienta encima del «Pagar» de la tarjeta de pagos. La regla que sale de ahí es que el contenido lleva 88 px de holgura inferior (56 del botón más sus dos márgenes de 16) en toda la app del cliente. Un flotante que tapa una acción no es un flotante: es un estorbo.

El flotante abierto

03 · el panel

380 px que nacen desde el botón: el círculo se estira en tarjeta desde su esquina inferior derecha, radio 50 % → 20 px, en 640 ms con --ease-out. Sin rebote. --ease-spring queda reservado para una sola cosa en toda la pieza: el tick de la estrella.

La corona es de noche, no la banda de día del home. El flotante es una capa encima de la página, no un bloque de ella — y la noche es la misma voz que le abrió la puerta al cliente en /login. Dos glows a la deriva: celeste arriba (22 s), lima abajo (31 s), amplitudes chicas porque es una corona de 380 px, no una escena de auth.

Los tres momentos · 380 px cada uno Se leen de izquierda a derecha: la primera vez que abres, el instante después de mandar, y la vuelta con respuesta. nuevo

El anuncio. Los chips no clasifican ni mandan: solo escriben las primeras palabras en el campo. Una caja vacía es lo más difícil de enfrentar; cuatro palabras ya puestas lo resuelven sin cobrar peaje.

Escribes primero, calificas después. Y solo la primera vez, nunca más. El «Saltar» es honesto: se puede saltar de verdad. El costo aceptado es que casi nadie califique — el texto vale más que el promedio.

El viaje de vuelta. Lo que se pidió hace dos semanas ya está hecho, y esa línea es la única con color propio en todo el flotante. Debajo, lo de ayer espera su turno con una promesa escrita, no con silencio. Tú hablas en burbuja; Mogos responde sobre el lienzo, sin caja.

La línea de estado Una sola línea que se reemplaza, nunca un riel de cinco pasos que se acumula.
EstadoSe ve asíQué lo dispara
EnviandoEnviando…Optimista, en el instante en que sueltas el botón
Enviada✓ Enviada · 14:32El 201 del API. No espera a Slack.
Vista👁Vista por el equipoAlguien reaccionó con 👀en Slack → seenAt
Anotada📝Anotada para hacerlaReacción status = REVIEWED
Lista✔ Lista · ya está en la appReacción 🚀status = IMPLEMENTED
La respuesta no es un estado. Antes esta línea decía «Respondida» cuando alguien escribía en el hilo — encima de la respuesta, que estaba ahí para leerse. Era decir dos veces lo mismo. La línea quedó libre para un solo eje, el avance, y así llega hasta el final. «Lista» es la única línea con color en todo el flotante, porque es la única que es una noticia; y el tick entra con --ease-spring en 240 ms — el segundo y último uso del rebote en la pieza, junto al de la estrella. En esta casa el resorte significa algo bueno acaba de pasar, y no significa nada más.
A 390 · el pulgar y los dos botones aurora-familia dejó el FAB de crear dentro de la barra de pestañas. El de sugerencias no puede pelear con él. nuevo

Columna distinta, plano distinto. El de crear vive dentro de la barra, centrado y con el degradado de marca. El de sugerencias baja a 48 px y se apoya sobre la barra, por la derecha. Nunca se tocan, y el pulgar sabe cuál es cuál sin pensarlo.

A 390 es pantalla completa, no una hoja a medias. Una hoja al 70 % deja media app asomando detrás y el teclado se come lo que queda. El pie con el aviso desaparece: en móvil el espacio es del texto.

Del otro lado: Slack

04 · el mensaje

Una Slack app propia, no un webhook entrante. Un webhook solo sabe escribir; acá hace falta leer reacciones y mensajes de hilo, que es exactamente de donde sale el «visto» y la respuesta. El mensaje trae todo lo que se necesita para actuar sin abrir otra pantalla.

El botón de WhatsApp solo existe si sirve

Aparece únicamente cuando phoneVerified es cierto. Si no lo es, el botón no está y una línea de contexto dice por qué. Un botón que lleva a un número sin verificar es peor que ninguno.

El enlace es wa.me/<dígitos>: el teléfono se guarda en E.164 con + y el repo ya sabe quitarlo — la regla vive escrita en apps/delivery/lib/whatsapp-intent.ts.

Y no se pinta de verde

El verde de WhatsApp es de otra marca. Slack ya pone el marco del botón; meterle un color ajeno a la paleta rompe el sistema por una convención que nadie pidió. El glifo basta para reconocerlo.

«Escribió desde» es el campo que más rinde: convierte un «no encuentro nada» en un bug reproducible sin tener que preguntar.

Un botón de enlace no es solo un enlace. Esto salió de leer la documentación de Block Kit, no de suponerlo: «If you're using url, you'll still receive an interaction payload and will need to send an acknowledgement response». Los dos botones de arriba disparan una interacción que la app tiene que contestar con un 200 en menos de tres segundos — y si no hay una Interactivity Request URL configurada, el enlace abre pero el usuario ve un error. No existe forma documentada de tener un botón de enlace puro.

Así que el diseño se queda igual —los botones siguen siendo dos y siguen sin pintarse de verde— pero la fase de construcción suma un endpoint que solo existe para contestar «recibido» y no hacer nada más. Va escrito en el código, o alguien lo borrará por muerto.
Las reacciones · qué mueve cada una Cuatro reacciones, dos ejes, y una sola frontera que no se cruza.
ReacciónQué mueve¿Lo ve el cliente?
👀 OjosseenAt = now — «Vista por el equipo»
Anotadastatus = REVIEWED — «Anotada para hacerla»
🚀 Cohetestatus = IMPLEMENTED — «Lista · ya está en la app» + insignia
🗑️ Papelerastatus = REJECTEDNo — la única que no sale del canal
💬 HiloCrea SuggestionReply — mensaje de Mogos + insignia
Si quitan la reacción: quitar 👀 no deshace nada — no se puede des-ver algo. Quitar o 🚀 sí revierte el estado: si alguien marcó «Lista» por error, el cliente está creyendo que se le entregó algo que no existe, y ese error es peor que el otro.
La única frontera es la papelera. Tres de las cuatro reacciones viajan al cliente, porque las tres son buenas noticias: te vimos, la anotamos, ya está. La cuarta no sale del canal nunca: el cliente jamás ve «rechazada». Un no silencioso es más amable que un no notificado — y quien quiera decir que no, puede escribirlo en el hilo con sus palabras, que es donde un no se puede explicar.

El riesgo de «Anotada». Es el único estado que es una promesa, y una promesa que se queda dos años en «Anotada» hace más daño que no haber dicho nada. Si el equipo la usa para archivar en vez de para comprometerse, esta reacción se retira y quedan dos. Es una decisión de disciplina, no de diseño.

El flujo, de punta a punta

05 · la mecánica

Seis pasos. El único que el cliente espera es el segundo: el resto pasa mientras él sigue con lo suyo.

01
cliente Escribe y suelta

El texto aparece en el hilo al instante como burbuja con «Enviando…». La interfaz no espera a nadie.

02
api POST /suggestions201

La fila se crea y el API responde de una. El userId sale del token, nunca del body — es el arreglo del que cuelga todo lo demás. Se guardan también route (desde dónde escribió) y el rating si ya lo dio. La línea de estado pasa a «Enviada · 14:32».

03
api slack El hub reparte, el descriptor arma

El servicio emite suggestion.created al INTEGRATION_HUB y ahí se acaba su responsabilidad — no nombra Slack. El hub busca quién atiende ese evento, llama a su render() puro y publica con chat.postMessage, ya fuera del camino de la respuesta. El ts que devuelve Slack se guarda como externalId en IntegrationDelivery: el ancla para casar después una reacción o un hilo con esta fila exacta.

04
slack Alguien reacciona

Events API pega en la puerta única POST /integrations/slack/events. El guardia verifica con la firma declarada en el descriptor, el parse() puro traduce el evento y se busca por (integrationKey, externalId). 👀escribe seenAt; y 🚀mueven status y sellan statusChangedAt. Gestos que el equipo ya hace por instinto, convertidos en las señales que el cliente esperaba.

05
slack db Alguien responde en el hilo

El evento trae thread_ts, que es el mismo externalId. Se guarda una fila en SuggestionReply con el texto y quién lo escribió. Si mañana quien escribe es un agente en vez de una persona, no cambia una sola pantalla: el hilo es el mismo.

06
cliente Abre el flotante y lo ve

GET /suggestions/mine al abrir, y ya. Sin tiempo real: nadie deja esto abierto esperando, y una sugerencia no es un chat en vivo. Si hay respuesta sin leer, el botón se lo dijo antes de abrirlo.

La puerta, no el atajo

06 · arquitectura

Slack es la primera, no la última. El mismo circuito lo van a querer después ventas, soporte y finanzas, y el destino no siempre será Slack. Así que la pregunta no es «cómo mando esto a Slack» sino cómo se manda cualquier cosa a cualquier parte sin volver a tocar el dominio.

Esta sección llega tarde a una conversación que ya existía. Mogos ya tiene un diseño aprobado para exactamente esto — «Conector de avisos a Slack y Lark» — con su interfaz MessagingConnector, su Card IR neutral, su patrón outbox con reintentos y sus cuatro capas para que el negocio nunca conozca Slack. No lo encontré porque inventarié el código y aquél es un diseño sin implementar.

Lo que sigue leyéndose bien como razonamiento, pero la recomendación es fusionar, no construir un módulo paralelo: aquel diseño llega a las mismas conclusiones y en dos puntos va más lejos —el Card IR, y tener a Lark dentro para demostrar de verdad que la abstracción es agnóstica—. suggestion.created debería entrar como un evento más de su catálogo. Lo que esta pieza sí aporta de forma independiente son los hallazgos verificados contra la documentación de Slack, que valen para cualquiera de las dos arquitecturas.
El atajo

SuggestionsService inyecta un SlackService y lo llama al crear la fila.

Funciona el lunes. El problema es que ahora el módulo de sugerencias sabe que Slack existe: el segundo destino ya no cabe sin editarlo, y el quinto obliga a editar los cinco dominios que notifican.

La puerta

SuggestionsService inyecta INTEGRATION_HUB y emite suggestion.created. No nombra Slack. No sabe que existe.

Del otro lado, un registro de descriptores decide quién atiende ese evento. Sumar Discord es un archivo y un bloque de env — cero líneas tocadas en suggestions/.

No es un patrón inventado para esta pieza: es el que el repo ya usa para los bancos. finance/banking/connectors/ lleva cinco bancos con la misma disciplina — el conector declara sus capacidades como datos, y el motor compartido lee esas declaraciones y nunca ramifica por banco. Es la única propiedad que hace barata la integración número siete.

render y parse son puras: se prueban sin red, sin base de datos y sin Nest. El motor las llama, hace la petición y aplica los efectos. Todo el I/O vive en un solo sitio.

La prueba de que la abstracción no es una apuesta Los cuatro webhooks que ya existen caben en la misma declaración de firma. Una abstracción que retroajusta lo escrito no es especulación.
WebhookEsquemaCadena que se firmaVentana
WhatsApp existehmac-sha256 · sha256=body
Instagram existehmac-sha256 · sha256=body
TikTok existehmac-sha256 · t=…,s=…{ts}.{body}300 s
Slack nuevahmac-sha256 · v0=v0:{ts}:{body}300 s
La deuda que esto deja de alimentar. Hoy hay cuatro guardias de firma copiados a mano — 389 líneas, sin una sola utilidad compartida en common/. Diffear el de Instagram contra el de WhatsApp con los nombres normalizados deja una sola línea de log distinta. Cada integración nueva vuelve a copiar. Con la firma declarada como dato, la quinta ya no copia — y las cuatro viejas tienen camino de migración. Pero no en el mismo PR: tocar el pipeline de mensajería mientras se estrena Slack es cambiar dos cosas a la vez. Va en fase aparte, con sus pruebas.
Sin bus de eventos

@nestjs/event-emitter, cqrs y bullmq no están instalados, y @OnEvent da cero resultados en todo el repo. Meter un bus para un solo consumidor es deuda neta.

El patrón de la casa para esto ya existe: el sumidero inyectable de BANK_ALERT_SINK. Con su regla, que acá es igual de obligatoria: una integración que falla no puede tumbar la operación. emit() nunca propaga — registra el fallo y devuelve.

Me equivoqué con slackTs

La sección anterior de esta misma propuesta proponía un campo slackTs en la tabla Suggestion. Eso es el nombre de un proveedor dentro de una tabla de dominio — justo la fuga que todo esto existe para impedir.

Se va a IntegrationDelivery, genérico: integrationKey, event, entityId, externalId, status, attempts. Suggestion se queda solo con lo suyo — y los reintentos por fin tienen dónde vivir.

Se llama integrations, no connectors. La palabra ya está tomada: finance/banking/connectors/ exporta registerConnector() y BankConnectorDescriptor. La colisión es de palabra y no de ruta — y por eso mismo es peor, porque dos registerConnector distintos en el mismo apps/api son una trampa para quien llegue en seis meses. Se replica el patrón, se cambia el sustantivo, y en la cabecera del módulo se cita el SDK bancario como el arte previo.

La decisión completa —con lo que se descarta y por qué— vive en docs/superpowers/specs/2026-08-08-integraciones-design.md, como ADR-API-001.

Lo que la base necesita

07 · el costo

Once cambios, ninguno grande. SuggestionStatusEnum no cambia ni un valor, y ahí está lo bonito: REVIEWED e IMPLEMENTED ya existían desde el primer día, esperando que alguien se los contara al cliente. Lo que faltaba no era vocabulario nuevo — era el otro eje (seenAt) y las fechas para saber qué es novedad.

CambioDóndePor qué
userId del tokensuggestions.controller.tsHoy toda sugerencia web es anónima. Sin esto no hay historial ni WhatsApp.
rating opcionalDTO · Prisma · ZodSe califica después de escribir, o nunca.
Largo del comentariocreate-suggestion.dto.tsEl 10/500 solo vive en el front; el API acepta cualquier cosa.
seenAtSuggestion«Vista» es otro eje que status: acuse de recibo, no gestión.
statusChangedAtSuggestionSin esto no se sabe si «Lista» es novedad o ya la vio.
suggestionsOpenedAtUserContra qué compara la insignia. En el usuario, no en localStorage: la novedad tiene que cruzar de teléfono a computadora.
routeSuggestionDesde qué pantalla escribió.
IntegrationDeliveryTabla nuevaEl ancla genérica: integrationKey, event, entityId, externalId (el ts de Slack), status, attempts. Ningún nombre de proveedor entra en Suggestion.
SuggestionReplyTabla nuevasuggestionId, body, slackUserName, createdAt.
GET /suggestions/minesuggestions.controller.tsEl historial del propio cliente.
POST /integrations/:key/eventsintegrations/La puerta única de entrada. Una ruta, un guardia, N proveedores.
El formulario de /suggestions sobrevive

Conviven a propósito. El flotante es la puerta rápida desde cualquier pantalla; el formulario es la puerta deliberada, con calificación de entrada, y sigue siendo el destino de los cinco puntos de entrada que ya lo apuntan.

Deuda anotada: dos interfaces sobre el mismo backend. Si el flotante se come el tráfico, el formulario se retira en una fase posterior — decisión de datos, no de diseño.

El anuncio muere en una fecha, no por deriva

El corte es una constante, no la primera visita de cada quien: el anuncio habla de la plataforma, no de la persona, y quien entre por primera vez en la semana 3 también lo merece.

Después de esa fecha el saludo pasa al permanente («¿Qué mejoramos?») sin que nadie tenga que acordarse de desactivar nada.

La receta, por fases

08 · el orden

Cada fase deja algo funcionando. Ninguna rompe lo que ya existe. Y la primera no es del flotante: es del andamio que lo va a sostener a él y a los que vengan.

Antes de la F1 hay una F0 de veinte minutos. Todo el «visto» cuelga de una pregunta que la documentación de Slack no contesta ni a favor ni en contra: ¿se dispara reaction_added cuando alguien reacciona a un mensaje publicado por el propio bot? La evidencia indirecta dice que sí —la doc explica qué forma tiene la carga cuando el autor no es una persona, lo cual solo tiene sentido si esos eventos existen— pero empíricamente funciona no es una fuente. Se prueba a mano en un workspace de juguete antes de escribir una línea. Si sale que no, el «visto» no puede venir de una reacción y se rediseña con un botón, que sí está documentado de punta a punta.
F1 El andamio de integraciones

integrations/ con el descriptor, el registro, el hub con su implementación de solo-registrar, los secretos por credentialRef y la tabla IntegrationDelivery. Más el arreglo del userId y los campos nuevos de Suggestion.

Queda funcionando: nada visible todavía. Es la fase que no se ve y sin la cual las otras tres se construyen torcidas.

F2 El circuito de ida

La Slack app, slack.integration.ts con su render(), el mensaje con sus dos botones, y el emit() en SuggestionsService.

Queda funcionando: el formulario que ya existe empieza a caer en Slack con nombre, ruta y WhatsApp. Todavía sin flotante — y ya vale la pena.

F3 El flotante y la vuelta

El botón, el panel, los chips, la calificación diferida, el historial, GET /mine. Y la puerta de entrada: controlador, guardia genérico, parse() de Slack, la insignia.

Queda funcionando: el encargo completo — el cliente se entera de que lo que pidió ya está hecho sin tener que ir a buscarlo.

F4 Cobrar la deuda PR aparte

Migrar WhatsApp, Instagram, TikTok y Supabase al guardia genérico: −389 líneas de guardias copiados.

Por qué aparte: tocar el pipeline de mensajería mientras se estrena Slack es cambiar dos cosas a la vez. Esta fase no bloquea a ninguna de las otras y puede esperar meses sin costo.

Un buzón recibe y calla.
Esto contesta.

Aurora · producto · sugerencias · agosto 2026