Todas las interfaces de chat autoalojadas te dan exactamente lo mismo: un muro de markdown. Le pides al modelo los números del trimestre pasado, te suelta una tabla en monospace, y si quieres un gráfico de verdad te toca copiar los números a otro sitio como si estuviéramos en 2011. Las más pijas hacen que el modelo escupa un PNG en base64, o sea la foto de un gráfico que se ha dibujado en la cabeza, con ejes inventados, en el que no puedes hacer clic, ni filtrar, ni confiar.
chatz hace lo que nadie se molesta en hacer: el asistente dibuja interfaz real y viva dentro del propio chat. No la foto de un gráfico. Un componente renderizado de verdad, responsive, construido por el navegador.
El asistente te dibuja el dashboard
Aquí va el mecanismo, porque es más tonto y mejor de lo que te imaginas. La respuesta del modelo puede traer un bloque ```spec, y dentro hay parches JSON RFC-6902. El navegador vigila el stream, pilla el bloque en el lado cliente, y monta esos parches en un componente vivo sacado de un catálogo fijo. El modelo no ejecuta código. No dibuja píxeles. Suelta una spec declarativa minúscula y el front la renderiza, igual que renderizaría cualquier otro componente.
El catálogo tiene 26 componentes (conté los ficheros, son exactamente 26), está cerrado y tipado, así que el modelo elige de un menú en vez de inventarse markup que igual parsea, igual no. Más allá del texto, el layout, los status y las tablas, es justo la analítica a la que querrías que un modelo echara mano: time-series, area, sparkline, bar, donut, funnel, gauge, scatter, heatmap, histogram, box-plot, treemap, un grafo de red y un log-viewer grande. Cada gráfico es SVG nativo, con cero dependencias de runtime de charting. Ni d3, ni chart.js, ni 400kb de librería enviada para dibujar una barra.
Y es honesto en lo único que importa: es un componente vivo alimentado con valores reales, no la foto de un gráfico que el modelo se ha inventado. Si una herramienta MCP vuelve con números, el modelo los mete directos en un gauge o en un treemap y te llevas algo que mirar, en vez de un párrafo que describe algo.
Un turno que no te miente sobre lo que ha pasado
El streaming es SSE al estilo Anthropic, un solo hilo del backend al navegador, y lo que me importa es que un turno se renderiza en el orden en que llega. El texto, el razonamiento y las llamadas a herramientas vienen entremezclados exactamente como los emite el modelo, text → tool → text → tool, en vez de amontonarse en un muro indistinguible al final como hacen casi todas las interfaces. El razonamiento, en los modelos que lo exponen, cae en su propio bloque plegable. Cada llamada a herramienta es una tarjeta que pasa por CALLING → DONE/ERROR, con su nombre, sus argumentos en streaming y su resultado. Ves la forma real del turno mientras ocurre.
Y no se come tu trabajo. Tu mensaje se guarda antes de que arranque el stream, así que darle a parar o refrescar a mitad de respuesta no evapora lo que acabas de escribir. Una respuesta que seguía en streaming se guarda como parcial, marcada explícitamente como interrumpida. Un refresco la conserva, pero nunca se cuela de tapadillo en una petición posterior al modelo como si estuviera completa. Cuando llega la respuesta de verdad, sustituye al checkpoint de forma atómica. Sin «uy, se ha perdido», sin medias respuestas haciéndose pasar por enteras.
Una sola cosa que levantar, y la base de datos que tú elijas
Ahora la parte de la que presume su propio README, con razón. Es una sola cosa que levantar. La interfaz va horneada dentro de la aplicación, así que no hay un front separado que compilar, alojar en algún sitio y volver a enganchar al backend. El chat y la API son el mismo container. Y lo levantas con Docker, que es como está pensado: te bajas la imagen multi-arch publicada (psyb0t/chatz en Docker Hub, con un tag fijado :v0.7.9, no :latest) y haces docker run con SQLite sobre un volumen, o haces docker compose up desde un checkout si quieres Postgres.
Y la ejecución que viene de fábrica está cerrada a cal y canto, no te la dejan de deberes. El docker run documentado va con filesystem root en solo lectura, --cap-drop ALL, usuario no root, no-new-privileges y límites de memoria, CPU y PID, y lo único en lo que puede escribir es /data y un fichero de log montado. Es de esos proyectos raros en los que el comando de despliegue que copias y pegas ya es el endurecido.
La persistencia es Postgres por defecto, o un modo SQLite de un solo proceso: pones CHATZ_DB_DRIVER=sqlite y listo, es un fichero en un volumen, sin servidor de base de datos que mantener vivo, para cuando solo estás tú. Y ese es todo el stack. Sin Temporal. Sin NATS. Sin message broker. Sin service mesh. Sin un compose de quince containers que te da miedo tocar. Es una app de chat y está construida como tal, lo cual en 2026 parece que cuenta como postura de diseño.
Las mierdas aburridas que sí ha clavado
Sacar una ventanita de chat sabe hacerlo cualquiera. Se nota en los trozos que la gente se salta:
- Los secretos MCP están cifrados de verdad. Añades servidores de herramientas MCP (stdio o HTTP), o importas un
.mcp.jsonal estilo Claude, y los secretos de las cabeceras HTTP y el env de stdio quedan sellados en reposo con AES-256-GCM bajo una claveCHATZ_SECRETS_KEYde 32 bytes. Nada de base64 y a rezar, cifrados de verdad, y si no pones la clave se niega en redondo a guardar el secreto antes que dejarlo en claro. Los valoresAuthorizationguardados siguen enmascarados en la interfaz de admin. - Aquí no se registra nadie solo. En el primer arranque la app está en estado de setup, con cero usuarios;
/setupcrea al único admin, y el admin da de alta a todos los demás. No hay registro público que se te pueda olvidar cerrar. ¿Estás solo?CHATZ_AUTH_PASSWORDLESS=trueloguea automáticamente al único admin, para que no tengas que teclear una contraseña para hablar con tu propia máquina. - El historial va con tope, para que un chat largo no acabe en una factura de cuatro cifras. Cada chat limita el historial que se envía (100.000 tokens por defecto). El mensaje de sistema más viejo se queda pegado y el turno actual se queda fijado; el historial más antiguo se va añadiendo de lo nuevo a lo viejo hasta que la siguiente unidad de mensaje entera se pasaría del tope. Solo unidades enteras, para que un resultado de herramienta nunca quede huérfano de la llamada que lo produjo. Y el cuadro de escritura te enseña la selección real del siguiente turno: el sistema pegado, el historial retenido, el borrador actual, los tokens gastados y libres, y qué turnos completos se han caído. Sin adivinar lo que estás pagando por enviar.
Todos tus proveedores, en una sola lista
Por aquí es por donde yo empezaría una demo. CHATZ_UPSTREAMS es un array JSON, y metes dentro tantos proveedores como quieras, todos vivos a la vez. Ollama moviendo modelos locales en tu máquina. aigate, tu propia pasarela por delante de lo que tengas alojado. Anthropic. z.ai. Cualquier endpoint compatible con OpenAI al que llegues. Chatz le pregunta a cada uno por su endpoint de modelos, descubre lo que sirve de verdad, y lo funde todo en un único selector.
Así que abres el desplegable de modelos y tu llama local está justo al lado de Claude, al lado de lo que haya detrás de tu pasarela, y elegir uno manda ese turno de vuelta al upstream del que salió. Cada upstream nombra su propio driver (openai, que también cubre las pasarelas compatibles con OpenAI, o anthropic) y su propia clave por nombre de variable de entorno, nunca a pelo, y cada driver está aislado de las credenciales de los demás para que la clave de un proveedor no se filtre en las llamadas de otro. Los alias legibles van por encima sin sustituir nunca al ID real del modelo, y los controles de razonamiento se apagan en los modelos que no anuncian soportarlo, para que el selector no te ofrezca un botón que no hace nada.
Un modo demo para cuando el modelo no colabora delante de la cámara
Detalle pequeño que demuestra que alguien ha intentado grabar esto de verdad: el modo showcase. Las demos con modelo en vivo se van de madre justo cuando se enciende la cámara, así que pones CHATZ_SHOWCASE_MODE=true y levantas el stack. Mantiene la lista real de modelos, la configuración MCP y el comportamiento del chat, pero intercepta ciertos prompts del catálogo con razonamiento determinista, actividad de herramientas sintética y luego los dashboards embebidos, y cada métrica que aparece está anclada en los resultados sintéticos visibles, no sacada de la manga. Las respuestas se guardan como en cualquier chat normal, así que una grabación puede refrescar o seguir. Es la diferencia entre enseñar interfaz generativa y rezar para que el modelo dibuje el gráfico que necesitas a la cuarta toma.
Es pronto, v0.7.9 y se mueve rápido. Pero la apuesta de fondo ya está ahí y es buena: un chat autoalojado donde el asistente renderiza componentes reales y vivos en vez de describirlos, entregado como un único container pequeño y endurecido (un solo binario Go con la interfaz horneada dentro) más la base de datos que elijas y nada más. Apúntalo a tus endpoints compatibles con OpenAI o Anthropic, engancha tus herramientas MCP, y deja que el modelo te dibuje un dashboard que no sea mentira.
github.com/psyb0t/chatz