El Event Bus
Vincent Depassier · 29 de septiembre de 2026
El Event Bus es la vía rápida. Lleva lo que está cambiando — un cursor que se mueve, alguien escribiendo, una fila que acaba de entrar — de un cliente conectado a los demás, en pocos milisegundos y sin escribir nada.
Esa última parte es todo el diseño, y es lo primero que tienes que decidir:
Si perder un mensaje importa, no va en el bus. Una automatización es durable: corre haya o no alguien mirando, reintenta y deja historial de ejecución. El bus es lo contrario: retransmite a quien esté conectado en ese momento. La prueba es una sola pregunta: si esto se pierde, ¿importa? Si la respuesta es sí, usa una automatización. Si es no, porque en 100 ms viene uno más nuevo, usa el bus.
Se combinan bien. Un mensaje de chat usa los dos: una automatización lo guarda y avisa a quienes no están, mientras el bus lo hace aparecer al instante para todos los que sí están mirando.
Hay una excepción, y es angosta: una ventana corta de reconexión para los buses ligados a una tabla. Ver Eventos de tabla en el Event Bus.
Conectarse
Un endpoint, una conexión, y sobre ella todos los buses que quieras:
wss://gateway.praxsuite.com/hubs/event-bus?access_token=<JWT de end user>Una API key no puede conectarse. El bus admite end users, no credenciales: la key es como tu backend inicia sesión por un usuario, y el token que recibes a cambio es lo que abre el socket. Una key sk_live_ en este endpoint se rechaza.
Todo esto se administra en API Gateway → Event Bus.

Claves de bus
Cada bus se direcciona como {topic}:{instancia}:
office:hq
cursor:doc-42
chat:8f3c1a90-5d2e-4c11-9b77-0e5a2c6d4471El topic lo declara una vez un administrador y gobierna todo: quién puede entrar, cuáles son los límites, si se anuncia la presencia. La instancia es libre, y es lo que permite que un topic sirva a miles de buses: uno por documento, uno por canal, uno por sala de juego.
Una clave cuyo topic nunca se declaró se rechaza. Es a propósito: nadie puede inventar un espacio de nombres y ponerse a escuchar ahí.
Dos cosas de la clave que te van a morder:
El topic se pasa a minúsculas; la instancia no.
Office:hqyoffice:hqson el mismo bus.chat:Room1ychat:room1son dos buses distintos, y los peers de uno nunca van a ver a los del otro, que se ve idéntico a un bug del cliente.`user:self` está reservado. Se resuelve en el servidor a tu propio bus privado. No puedes direccionar el de otra persona: nombrar el de otro usuario se rechaza de plano, no se redirige en silencio.
Quién puede entrar
Lo decide el modo de acceso del topic. Escalan en costo, y los dos primeros no necesitan ninguna consulta.
Modo | Admite | Para qué |
Workspace | cualquier end user autenticado del workspace | cursores, indicadores de escritura, una sala abierta |
Roles | quien tenga alguno de los roles permitidos del topic | canales solo para staff, un feed de administración |
Grants | quien tenga un grant sobre esa instancia puntual | un canal de chat privado y el siguiente abierto |
Ticket | quien presente un ticket de ingreso firmado | reglas por recurso que decide tu propia lógica |
Grants es el modo cuando los permisos cambian por bus y no por topic. Roles no puede expresar eso: los roles permitidos viven en el topic, así que chat:a y chat:b los compartirían por fuerza. Grants reutiliza los mismos grants de recurso contra los que autoriza el resto de la plataforma, así que un recurso que declaraste una vez ya es alcanzable sin declararlo dos. Como todo grant, falla cerrado: una instancia sin grants se deniega, nunca queda pública.
Dos trampas en modo Grants:
La instancia tiene que ser un GUID, porque eso es el id de recurso de un grant.
chat:generalno se puede otorgar;chat:{guid}sí.Una instancia sin ningún grant se rechaza, no se abre.
El modo Ticket está declarado pero hoy no tiene emisor. Nada en el producto emite un ticket de ingreso — ni un endpoint, ni un nodo de automatización, ni una herramienta — así que a un topic en modo Ticket no puede entrar nadie. Usa Grants hasta que eso cambie.
El modo de acceso, las tablas fuente, la presencia, el estado retenido y los límites se fijan todos donde se declara el tópico:

Cómo se habla con él
Cuatro llamadas.
JoinBus(busKey, ticket) -> { ok, error, peers[] }
JoinBusSince(busKey, ticket, sinceCursor) -> { ok, error, peers[], missed[] }
LeaveBus(busKey)
Publish(busKey, eventName, payload) -> { ok, error, recipients, cursor }JoinBusSince es JoinBus más la ventana de reconexión que describe la página de eventos de tabla. Usa JoinBus mientras no tengas un cursor desde el cual retomar; en todo lo demás son idénticas.
Manda todos los argumentos que declara la llamada, incluso aquellos para los que no tienes valor. El transporte reconoce una llamada por la cantidad de argumentos que enviaste, no por sus nombres, así que JoinBus(busKey) sin el ticket no se completa con un valor por defecto: no calza, y vuelve como un error genérico del servidor que no dice nada de la causa real. Manda null: JoinBus(busKey, null).
Esa misma regla es la razón de que el cursor de reconexión tenga su propia llamada en vez de ser un tercer argumento de JoinBus. Agregarlo ahí habría roto a todo cliente ya construido contra la forma de dos argumentos, que es exactamente lo que pasó durante un día en septiembre de 2026.
Y cuatro cosas que el servidor te manda:
Evento | Payload | Cuándo |
|
| alguien publicó |
|
| entró un peer, si la presencia está activa |
|
| salió un peer, si la presencia está activa |
|
| te sacaron con la conexión abierta |
Cada mensaje trae su `bus`, y lo vas a necesitar. Una conexión lleva todos los buses a los que entraste, y el transporte te dice qué llamada llegó, nunca de qué bus vino. Sin ese campo, un cliente en dos buses no puede distinguir un evento de office:hq de uno de cursor:doc-42.
Quien publica no recibe su propio evento. Publish va a los demás del bus. Actualiza tu propia interfaz localmente; no esperes el eco.
`bus-evicted` existe porque un socket sobrevive al chequeo de permisos. Si un topic se desactiva o se re-scopea mientras estás conectado, te sacan activamente en vez de dejarte escuchando en silencio.
Estado retenido
Si el topic retiene estado, se guarda el último mensaje de cada peer y se le entrega al siguiente que entra, como peers[] en el resultado del join. Eso es lo que hace funcionar a un topic de cursores: quien llega tarde ve dónde está cada uno al instante, en vez de esperar a que todos se muevan.
Es último-que-escribe-gana por peer, no un historial, y está topeado por peer: un peer que publica un payload más grande que el tope simplemente no retiene estado, en vez de poder fijar memoria.
Publicar desde un servidor
No necesitas un socket para anunciar algo. Una credencial del gateway puede publicar por HTTP:
POST https://gateway.praxsuite.com/api/v1/gateway/{workspaceId}/bus/{topic}/{instancia}/publish
Authorization: Bearer sk_live_...
Content-Type: application/json
{ "event": "order.paid", "payload": { "orderId": "A-1187" } }event es obligatorio. El topic y la instancia son segmentos separados de la ruta a propósito, para que el escapado de ningún cliente HTTP pueda convertir una clave válida en otra distinta.
Límites
Estos son los techos de la plataforma. Un topic puede fijar un valor más bajo en los tres primeros.
Límite | Por defecto |
Publicaciones por segundo, por conexión | 60 |
Mensajes salientes por segundo, por conexión | 600 (ráfaga 1200) |
Tamaño de payload | 8 KB |
Peers en un bus | 200 |
Buses que una conexión puede sostener | 20 |
Largo de la clave de bus | 128 caracteres |
Largo del nombre de evento | 64 caracteres |
Estado retenido por peer | 2 KB |
Una publicación se cobra por destinatario, no por llamada. Sesenta publicaciones por segundo en un bus de 200 peers son doce mil mensajes salientes, y eso es lo que cuenta el presupuesto. Un bus tranquilo casi no cuesta; uno ruidoso en un bus lleno se queda sin cupo mucho antes de quedarse sin llamadas.
Cuando una llamada se rechaza
ok vuelve en false con un error sobre el que puedes ramificar.
Error | Qué significa |
| mal formada, vacía o pasada de largo |
| el topic nunca se declaró |
| está declarado, pero apagado |
| modo Roles, y no tienes ninguno de ellos |
| modo Grants, y no tienes grant sobre esta instancia |
| modo Ticket, y la firma no dio |
| el bus está lleno, o ya tienes tu máximo de buses |
| publicaste en un bus al que nunca entraste |
| vacío, o pasado de largo |
| pasa el tope de payload del topic |
| gastaste tu cupo |
Un rechazo siempre llega así, como ok: false con un nombre que puedes leer. Una falla que en cambio vuelve como un escueto "error en el servidor" no es ninguno de estos: significa que la llamada nunca calzó, y lo primero que hay que revisar es la cantidad de argumentos.
Lo que el bus no es
No es almacenamiento. No se persiste nada, salvo la ventana corta de reconexión que describe la página de eventos de tabla. Un mensaje que nadie estaba para escuchar se perdió, y eso es el comportamiento correcto.
No es una cola. Sin acuses, sin reintentos, sin dead letters.
No es confiable. Un payload publicado por un peer lo escribió otro cliente y se retransmite tal cual: nunca se parsea, nunca se valida. Trátalo como entrada hostil, igual que el cuerpo de una petición.
No reemplaza a las automatizaciones. Si quien recibe es tu sistema y no una persona mirando una pantalla, probablemente quieras una automatización.
Siguiente
Eventos de tabla en el Event Bus
API Gateway: visión general
Roles y permisos
Automatizaciones: nodos de disparo