chatz: Ein Self-Hosted KI-Chat, in dem der Assistent dir das verdammte Dashboard malt

Jede self-hosted Chat-Oberfläche liefert dir exakt dasselbe: eine Wand aus Markdown. Du fragst das Modell nach den Zahlen vom letzten Quartal, es wirft dir eine Tabelle in Monospace hin, und wenn du ein echtes Diagramm willst, kopierst du die Zahlen woanders hin, als wäre 2011. Die feineren lassen das Modell ein base64-PNG ausspucken, also das Foto eines Diagramms, das es sich im Kopf gemalt hat, mit erfundenen Achsen, auf das du weder klicken noch filtern noch dich verlassen kannst.
chatz macht das, was sich sonst keiner antut: Der Assistent zeichnet echte, lebende UI direkt in den Chat. Kein Bild von einem Diagramm. Eine wirklich gerenderte, responsive Komponente, gebaut vom Browser.

Der Assistent malt dir das Dashboard

Hier der Mechanismus, denn er ist dümmer und besser, als du vermutest. Die Antwort des Modells kann einen ​```spec-Block enthalten, und darin stecken RFC-6902-JSON-Patches. Der Browser beobachtet den Stream, erkennt den Block clientseitig und setzt die Patches zu einer lebenden Komponente aus einem festen Katalog zusammen. Das Modell führt keinen Code aus. Es malt keine Pixel. Es gibt eine kleine deklarative Spec aus, und das Frontend rendert sie, genau wie jede andere Komponente.
Der Katalog hat 26 Komponenten (ich habe die Dateien gezählt, es sind exakt 26), er ist geschlossen und typisiert, also wählt das Modell aus einem Menü, statt Markup zu erfinden, das vielleicht parst und vielleicht nicht. Jenseits von Text, Layout, Status und Tabellen ist es genau die Analytik, nach der ein Modell greifen soll: Time-Series, Area, Sparkline, Bar, Donut, Funnel, Gauge, Scatter, Heatmap, Histogram, Box-Plot, Treemap, ein Netzwerkgraph und ein großer Log-Viewer. Jedes Diagramm ist natives SVG, ohne jede Charting-Runtime als Abhängigkeit. Kein d3, kein chart.js, keine 400kb Bibliothek mitgeliefert, nur um einen Balken zu malen.
Und es ist ehrlich in dem einen Punkt, der zählt: Es ist eine lebende Komponente, gefüttert mit echten Werten, nicht das Foto eines halluzinierten Diagramms. Kommt ein MCP-Tool mit Zahlen zurück, stopft das Modell sie direkt in ein Gauge oder eine Treemap, und du bekommst etwas zum Anschauen statt eines Absatzes, der etwas beschreibt.

Ein Zug, der nicht lügt, was passiert ist

Das Streaming ist SSE im Anthropic-Stil, eine einzige Leitung vom Backend zum Browser, und was mich interessiert: Ein Zug rendert in der Reihenfolge, in der er ankommt. Text, Reasoning und Tool-Calls kommen genau so verschränkt, wie das Modell sie ausgibt, text → tool → text → tool, statt am Ende zu einer ununterscheidbaren Wand zusammengeschoben zu werden, wie es die meisten Oberflächen machen. Reasoning fließt bei Modellen, die es offenlegen, in seinen eigenen aufklappbaren Block. Jeder Tool-Call ist eine Karte, die durch CALLING → DONE/ERROR läuft, mit Name, gestreamten Argumenten und Ergebnis. Du siehst die echte Form des Zuges entstehen.
Und es frisst dir nicht deine Arbeit. Deine Nachricht wird gespeichert, bevor der Stream überhaupt startet, also verdampft Stop drücken oder mitten in der Antwort neu laden nicht das, was du gerade getippt hast. Eine noch streamende Antwort wird als Teilstück gesichert, ausdrücklich als unterbrochen markiert. Ein Reload behält sie, aber sie wird nie klammheimlich in eine spätere Anfrage ans Modell geschoben, als wäre sie fertig. Wenn die echte Antwort landet, ersetzt sie den Checkpoint atomar. Kein „ups, weg“, keine halbe Antwort, die sich als ganze ausgibt.

Eine Sache zum Starten, und die Datenbank deiner Wahl

Jetzt der Teil, mit dem das eigene README angibt, zu Recht. Es ist eine Sache zum Starten. Die Oberfläche ist in die Anwendung eingebacken, es gibt also kein separates Frontend, das gebaut, irgendwo gehostet und wieder ans Backend geklebt werden muss. Chat und API sind derselbe Container. Und du startest es mit Docker, so wie gedacht: Du ziehst das veröffentlichte Multi-Arch-Image (psyb0t/chatz auf Docker Hub, ein fester Tag :v0.7.9, nicht :latest) und machst docker run mit SQLite auf einem Volume, oder docker compose up aus einem Checkout, wenn du Postgres willst.
Und der ausgelieferte Lauf ist von Haus aus dichtgemacht, nicht als Übungsaufgabe hinterlassen. Das dokumentierte docker run fährt mit read-only Root-Filesystem, --cap-drop ALL, Non-Root-User, no-new-privileges und Grenzen für Speicher, CPU und PIDs, und schreiben darf es nur nach /data und in eine gemountete Logdatei. Es ist die seltene Sorte Projekt, bei der der Deploy-Befehl zum Kopieren schon der gehärtete ist.
Persistenz ist standardmäßig Postgres, oder ein SQLite-Modus mit einem einzigen Prozess: CHATZ_DB_DRIVER=sqlite setzen und fertig, es ist eine Datei auf einem Volume, kein Datenbankserver, den du am Leben halten musst, für wenn du allein bist. Und das ist der ganze Stack. Kein Temporal. Kein NATS. Kein Message Broker. Kein Service Mesh. Kein Fünfzehn-Container-Compose, das du dich nicht anzufassen traust. Es ist eine Chat-App und sie ist wie eine gebaut, was 2026 offenbar schon als Designhaltung durchgeht.

Der langweilige Scheiß, den es wirklich hinbekommen hat

Eine Chatbox ausliefern kann jeder. Man sieht es an den Teilen, die alle überspringen:

  • MCP-Secrets sind wirklich verschlüsselt. Du fügst MCP-Tool-Server hinzu (stdio oder HTTP) oder importierst eine .mcp.json im Claude-Stil, und die Secrets aus den HTTP-Headern und das stdio-Env werden im Ruhezustand mit AES-256-GCM unter einem 32-Byte-Schlüssel CHATZ_SECRETS_KEY versiegelt. Nicht base64 und ein Stoßgebet, wirklich verschlüsselt, und ohne gesetzten Schlüssel weigert es sich rundheraus, das Secret zu speichern, statt es im Klartext liegen zu lassen. Gespeicherte Authorization-Werte bleiben in der Admin-Oberfläche maskiert.
  • Hier meldet sich niemand selbst an. Beim ersten Start ist die App im Setup-Zustand mit null Nutzern; /setup legt den einzigen Admin an, und der Admin legt alle anderen an. Es gibt keine öffentliche Registrierung, die man zu schließen vergessen könnte. Allein unterwegs? CHATZ_AUTH_PASSWORDLESS=true loggt den einen Admin automatisch ein, damit du kein Passwort tippst, um mit deiner eigenen Kiste zu reden.
  • Die History ist gedeckelt, damit ein langer Chat keine vierstellige Rechnung wird. Jeder Chat begrenzt die ausgehende History (standardmäßig 100.000 Tokens). Die älteste System-Nachricht bleibt klebend und der aktuelle Zug bleibt angepinnt; ältere History kommt von neu nach alt dazu, bis die nächste ganze Nachrichteneinheit das Limit sprengen würde. Nur ganze Einheiten, damit ein Tool-Ergebnis nie von dem Call getrennt wird, der es erzeugt hat. Und das Eingabefeld zeigt dir die echte Auswahl für den nächsten Zug: klebendes System, behaltene History, aktueller Entwurf, verbrauchte und freie Tokens, und welche vollständigen Züge rausgefallen sind. Kein Raten, wofür du beim Senden bezahlst.

Alle Provider, die du hast, in einer Liste

Damit würde ich eine Demo anfangen. CHATZ_UPSTREAMS ist ein JSON-Array, und du stopfst so viele Provider hinein, wie du willst, alle gleichzeitig aktiv. Ollama, das lokale Modelle auf deiner Kiste dreht. aigate, dein eigenes Gateway vor dem, was du selbst hostest. Anthropic. z.ai. Jeder OpenAI-kompatible Endpoint, den du erreichst. Chatz fragt bei jedem den Modell-Endpoint ab, findet heraus, was er tatsächlich ausliefert, und verschmilzt alles zu einem einzigen Picker.
Du öffnest also das Modell-Dropdown, und dein lokales Llama sitzt direkt neben Claude, neben dem, was hinter deinem Gateway hängt, und eine Auswahl schickt den Zug direkt zurück an den Upstream, aus dem er kam. Jeder Upstream benennt seinen eigenen Driver (openai, das auch OpenAI-kompatible Gateways abdeckt, oder anthropic) und seinen eigenen Schlüssel über den Namen der Umgebungsvariable, nie inline, und jeder Driver ist von den Credentials der anderen abgeschottet, damit der Schlüssel des einen Providers nicht in die Calls eines anderen sickert. Freundliche Aliase liegen obendrauf, ohne je die echte Modell-ID zu ersetzen, und Reasoning-Regler werden bei Modellen ausgegraut, die keine Unterstützung melden, damit der Picker dir keinen Knopf anbietet, der nichts tut.

Ein Demo-Modus für wenn das Modell vor der Kamera nicht mitspielt

Kleinigkeit, die zeigt, dass jemand wirklich versucht hat, das aufzunehmen: der Showcase-Modus. Demos mit Live-Modell entgleisen genau dann, wenn die Kamera läuft, also setzt du CHATZ_SHOWCASE_MODE=true und fährst den Stack hoch. Er behält die echte Modell-Liste, das MCP-Setup und das Chat-Verhalten, fängt aber bestimmte Katalog-Prompts ab, mit deterministischem Nachdenken, synthetischer Tool-Aktivität und dann den eingebetteten Dashboards, wobei jede angezeigte Kennzahl in den sichtbaren synthetischen Ergebnissen verankert ist, nicht aus der Luft gegriffen. Die Antworten werden gespeichert wie in jedem normalen Chat, eine Aufnahme kann also neu laden oder weiterlaufen. Das ist der Unterschied zwischen generative UI vorführen und beten, dass das Modell dir im vierten Take das Diagramm malt, das du brauchst.


Es ist früh, v0.7.9 und in Bewegung. Aber die Grundwette steht schon und sie ist gut: ein self-hosted Chat, in dem der Assistent echte, lebende Komponenten rendert, statt sie zu beschreiben, ausgeliefert als ein kleiner gehärteter Container (ein einziges Go-Binary mit eingebackener Oberfläche) plus die Datenbank deiner Wahl und sonst nichts. Richte ihn auf deine OpenAI- oder Anthropic-kompatiblen Endpoints, häng deine MCP-Tools dran, und lass das Modell dir ein Dashboard malen, das keine Lüge ist.
github.com/psyb0t/chatz