Toutes les interfaces de chat auto-hébergées te servent exactement la même chose: un mur de markdown. Tu demandes au modèle les chiffres du trimestre dernier, il te pond un tableau en monospace, et si tu veux un vrai graphique tu vas coller les chiffres ailleurs comme si on était en 2011. Les plus frimeuses font cracher au modèle un PNG en base64, c’est-à-dire la photo d’un graphique qu’il a dessiné dans sa tête, avec des axes inventés, sur lequel tu ne peux pas cliquer, que tu ne peux pas filtrer, et auquel tu ne peux pas te fier.
chatz fait le truc que personne ne se donne la peine de faire: l’assistant dessine de l’interface réelle et vivante directement dans le chat. Pas l’image d’un graphique. Un vrai composant rendu, responsive, construit par le navigateur.
L’assistant te dessine le dashboard
Voilà le mécanisme, parce qu’il est plus bête et meilleur que tu ne l’imagines. La réponse du modèle peut contenir un bloc ```spec, et dedans il y a des patches JSON RFC-6902. Le navigateur surveille le stream, repère le bloc côté client, et assemble ces patches en composant vivant à partir d’un catalogue fixe. Le modèle n’exécute pas de code. Il ne dessine pas de pixels. Il émet une petite spec déclarative et le front la rend, exactement comme il rendrait n’importe quel autre composant.
Le catalogue compte 26 composants (j’ai compté les fichiers, il y en a exactement 26), il est fermé et typé, donc le modèle choisit dans un menu au lieu d’inventer du markup qui parsera peut-être. Au-delà du texte, du layout, des status et des tableaux, c’est exactement l’analytique vers laquelle tu voudrais qu’un modèle se tourne: time-series, area, sparkline, bar, donut, funnel, gauge, scatter, heatmap, histogram, box-plot, treemap, un graphe réseau et un gros log-viewer. Chaque graphique est en SVG natif, sans la moindre dépendance de runtime de charting. Pas de d3, pas de chart.js, pas de 400ko de bibliothèque embarquée pour dessiner une barre.
Et c’est honnête sur le seul point qui compte: c’est un composant vivant alimenté par de vraies valeurs, pas la photo d’un graphique halluciné. Si un outil MCP revient avec des chiffres, le modèle les fourre directement dans un gauge ou un treemap et tu obtiens quelque chose à regarder, au lieu d’un paragraphe qui décrit quelque chose.
Un tour qui ne ment pas sur ce qui s’est passé
Le streaming est du SSE façon Anthropic, un seul fil du backend au navigateur, et ce qui m’intéresse c’est qu’un tour se rend dans l’ordre d’arrivée. Le texte, le raisonnement et les appels d’outils arrivent entremêlés exactement comme le modèle les émet, text → tool → text → tool, au lieu d’être entassés en un bloc indistinct à la fin comme le font la plupart des interfaces. Le raisonnement, chez les modèles qui l’exposent, coule dans son propre bloc repliable. Chaque appel d’outil est une carte qui passe par CALLING → DONE/ERROR, avec son nom, ses arguments streamés et son résultat. Tu vois la forme réelle du tour se dérouler.
Et ça ne te bouffe pas ton travail. Ton message est sauvegardé avant que le stream démarre, donc appuyer sur stop ou rafraîchir au milieu d’une réponse ne volatilise pas ce que tu viens d’écrire. Une réponse encore en train de streamer est enregistrée comme partielle, explicitement marquée interrompue. Un refresh la conserve, mais elle n’est jamais glissée en douce dans une requête ultérieure au modèle comme si elle était complète. Quand la vraie réponse arrive, elle remplace le checkpoint de façon atomique. Pas de «oups, c’est perdu», pas de demi-réponse qui se fait passer pour finie.
Une seule chose à lancer, et la base de données que tu veux
Maintenant la partie dont son propre README se vante, à raison. C’est une seule chose à lancer. L’interface est cuite dans l’application, donc il n’y a pas de front séparé à builder, à héberger quelque part et à rebrancher sur le backend. Le chat et l’API sont le même container. Et tu le lances avec Docker, comme prévu: tu tires l’image multi-arch publiée (psyb0t/chatz sur Docker Hub, un tag épinglé :v0.7.9, pas :latest) et tu fais docker run avec SQLite sur un volume, ou tu fais docker compose up depuis un checkout si tu veux Postgres.
Et la commande livrée est verrouillée d’origine, pas laissée en exercice. Le docker run documenté part avec un filesystem root en lecture seule, --cap-drop ALL, un user non-root, no-new-privileges, et des limites de mémoire, de CPU et de PID, avec pour seules zones inscriptibles /data et un fichier de log monté. C’est le genre rare de projet où la commande de déploiement que tu copies-colles est déjà la version durcie.
La persistance c’est Postgres par défaut, ou un mode SQLite mono-processus: tu mets CHATZ_DB_DRIVER=sqlite et voilà, c’est un fichier sur un volume, aucun serveur de base à maintenir en vie, pour quand il n’y a que toi. Et c’est toute la stack. Pas de Temporal. Pas de NATS. Pas de message broker. Pas de service mesh. Pas de compose à quinze containers auquel tu as peur de toucher. C’est une app de chat, et elle est construite comme telle, ce qui en 2026 passe apparemment pour une prise de position.
Les trucs chiants qu’il a vraiment réussis
Livrer une boîte de chat, n’importe qui sait faire. Ça se voit dans les morceaux que les gens sautent:
- Les secrets MCP sont vraiment chiffrés. Tu ajoutes des serveurs d’outils MCP (stdio ou HTTP), ou tu importes un
.mcp.jsonfaçon Claude, et les secrets des en-têtes HTTP et l’env stdio sont scellés au repos en AES-256-GCM sous une cléCHATZ_SECRETS_KEYde 32 octets. Pas du base64 et une prière, vraiment chiffrés, et si tu ne mets pas la clé il refuse catégoriquement de stocker le secret plutôt que de le garder en clair. Les valeursAuthorizationenregistrées restent masquées dans l’interface d’admin. - Personne ne s’inscrit tout seul. Au premier lancement l’app est en état de setup, zéro utilisateur;
/setupcrée l’unique admin, et c’est l’admin qui crée tous les autres. Il n’y a pas d’inscription publique que tu pourrais oublier de fermer. Tout seul?CHATZ_AUTH_PASSWORDLESS=trueconnecte automatiquement l’unique admin, comme ça tu ne tapes pas un mot de passe pour parler à ta propre machine. - L’historique est plafonné, pour qu’un long chat ne devienne pas une facture à quatre chiffres. Chaque chat limite l’historique envoyé (100 000 tokens par défaut). Le plus ancien message système reste collé et le tour courant reste épinglé; l’historique plus ancien s’ajoute du plus récent au plus ancien jusqu’à ce que la prochaine unité de message entière dépasse le plafond. Uniquement des unités entières, pour qu’un résultat d’outil ne se retrouve jamais orphelin de l’appel qui l’a produit. Et la zone de saisie t’affiche la sélection réelle pour le tour suivant: le système collé, l’historique retenu, le brouillon en cours, les tokens consommés et libres, et quels tours complets sont passés à la trappe. Pas besoin de deviner ce que tu paies pour envoyer.
Tous tes providers, dans une seule liste
C’est par là que je commencerais une démo. CHATZ_UPSTREAMS est un tableau JSON, et tu y entasses autant de providers que tu veux, tous actifs en même temps. Ollama qui fait tourner des modèles locaux sur ta machine. aigate, ta propre passerelle devant ce que tu héberges. Anthropic. z.ai. N’importe quel endpoint compatible OpenAI que tu peux joindre. Chatz demande à chacun son endpoint de modèles, découvre ce qu’il sert vraiment, et fond le tout dans un seul sélecteur.
Donc tu ouvres la liste des modèles et ton llama local est posé juste à côté de Claude, à côté de ce qu’il y a derrière ta passerelle, et en choisir un renvoie le tour directement vers l’upstream d’où il vient. Chaque upstream nomme son propre driver (openai, qui couvre aussi les passerelles compatibles OpenAI, ou anthropic) et sa propre clé par nom de variable d’environnement, jamais en dur, et chaque driver est isolé des identifiants des autres pour que la clé d’un provider ne fuite pas dans les appels d’un autre. Les alias lisibles se posent par-dessus sans jamais remplacer l’ID réel du modèle, et les contrôles de raisonnement s’éteignent pour les modèles qui n’annoncent pas le supporter, comme ça le sélecteur ne te propose pas un bouton qui ne fait rien.
Un mode démo pour quand le modèle refuse de coopérer devant la caméra
Petit détail qui montre que quelqu’un a vraiment essayé d’enregistrer ce truc: le mode showcase. Les démos avec modèle live partent en vrille pile au moment où la caméra tourne, donc tu mets CHATZ_SHOWCASE_MODE=true et tu montes la stack. Il garde la vraie liste de modèles, la config MCP et le comportement du chat, mais intercepte certains prompts du catalogue avec un raisonnement déterministe, de l’activité d’outils synthétique, puis les dashboards intégrés, chaque métrique affichée étant ancrée dans les résultats synthétiques visibles, pas sortie du chapeau. Les réponses se sauvegardent comme dans n’importe quel chat, donc un enregistrement peut rafraîchir ou continuer. C’est la différence entre démontrer de l’interface générative et prier pour que le modèle dessine le graphique dont tu as besoin à la quatrième prise.
C’est jeune, v0.7.9 et ça bouge vite. Mais le pari de départ est déjà là et il est bon: un chat auto-hébergé où l’assistant rend des composants réels et vivants au lieu de les décrire, livré comme un seul petit container durci (un binaire Go unique avec l’interface cuite dedans) plus la base de données de ton choix et rien d’autre. Pointe-le vers tes endpoints compatibles OpenAI ou Anthropic, branche tes outils MCP, et laisse le modèle te dessiner un dashboard qui n’est pas un mensonge.
github.com/psyb0t/chatz