chatz: A Self-Hosted AI Chat Where the Assistant Draws the Fucking Dashboard

Every self-hosted chat UI hands you the same thing: a wall of markdown. You ask the model for last quarter’s numbers, it writes you a table in monospace, and if you want an actual chart you go paste the numbers into something else like it’s 2011. The fancy ones will have the model spit out a base64 PNG of a chart, which is a screenshot of a graph the model drew in its head, with made-up axes, that you cannot click, filter, or trust.
chatz does the thing nobody bothers with: the assistant draws real, live UI right in the chat. Not a picture of a chart. An actual rendered, responsive component the browser built.

The assistant draws the dashboard

Here’s the mechanism, because it’s dumber and better than you’d guess. The model’s reply can include a fenced ​```spec block, and inside it are RFC-6902 JSON patches. The browser watches the stream, spots the fence client-side, and assembles those patches into a live component out of a fixed catalog. The model isn’t running code. It isn’t drawing pixels. It’s emitting a little declarative spec and the front end renders it, the same way it’d render any other component.
The catalog is 26 components (I counted the files, it’s exactly 26), and it’s closed and typed, so the model picks from a menu instead of inventing markup that may or may not parse. Past the basic text, layout, status and table stuff, it’s the analytics you’d actually want a model to reach for: time-series, area, sparkline, bar, donut, funnel, gauge, scatter, heatmap, histogram, box-plot, treemap, a network graph, and a big log-viewer. Every chart is native SVG with zero charting-runtime dependency. No d3, no chart.js, no 400kb of library shipped to draw a bar.
And it’s honest in the one way that matters: it’s an actual live component fed real values, not a screenshot of a chart the model hallucinated. If an MCP tool comes back with numbers, the model can shove those numbers straight into a gauge or a treemap and you get a thing you can look at, instead of a paragraph describing a thing.

A turn that doesn’t lie about what happened

The streaming is Anthropic-style SSE, one wire from backend to browser, and the thing I care about is that a turn renders in arrival order. Text, reasoning, and tool calls interleave exactly as the model emits them, text → tool → text → tool, instead of getting clumped into one undifferentiated wall at the end like most UIs do. Reasoning from models that expose it streams into its own collapsible thinking block. Each tool call is a card that goes CALLING → DONE/ERROR with its name, streamed args, and result. You watch the actual shape of the turn happen.
And it doesn’t eat your work. The user message is saved before the stream even starts, so hitting stop or refreshing mid-answer doesn’t vaporise what you just typed. A streaming assistant reply gets checkpointed as an explicitly-interrupted partial. A refresh preserves it, but it’s never quietly replayed into a later model request as if it were complete. When the real answer lands it atomically replaces the checkpoint. No “oops, lost that,” no half-answer masquerading as a finished one.

One thing to run, a database you pick

Now the part its own README is smug about, correctly. It’s one thing to run. The UI is baked into the app itself, so there’s no separate frontend to build, host somewhere, and wire back to the backend. The chat and the API are the same container. And you run it with Docker, the way it’s meant to run: you pull the published multi-arch image (psyb0t/chatz on Docker Hub, a pinned :v0.7.9 tag, not :latest) and docker run it with SQLite on a volume, or you docker compose up from a checkout when you want Postgres.
And the shipped run is locked down by default, not left as an exercise. The documented production docker run goes read-only root filesystem, --cap-drop ALL, non-root user, no-new-privileges, and bounded memory, CPU and PID limits, with /data and one bind-mounted log file as the only things it can write. It’s the rare project where the copy-paste deploy command is already the hardened one.
Persistence is Postgres by default, or a single-process SQLite mode: flip CHATZ_DB_DRIVER=sqlite and it’s just a file on a volume, no database server to run, for when it’s only you. And that’s the whole stack. No Temporal. No NATS. No message broker. No service mesh. No fifteen-container compose you’re scared to touch. It’s a chat app, and it’s built like one, which in 2026 apparently counts as a design stance.

The boring shit it actually got right

Anyone can ship a chat box. The tells are in the parts people skip:

  • MCP secrets are actually encrypted. You add MCP tool servers (stdio or HTTP), or import a Claude-style .mcp.json, and their HTTP header secrets and stdio env get sealed at rest with AES-256-GCM under a 32-byte CHATZ_SECRETS_KEY. Not base64-and-a-prayer, actually encrypted, and if you don’t set the key it flat refuses to store the secret rather than keep it in plaintext. Stored Authorization values stay masked in the admin UI.
  • Nobody signs themselves up. First run, the app is in setup state with zero users; /setup makes the sole admin, and the admin provisions everyone else. There’s no public registration to forget to lock down. Single-user? CHATZ_AUTH_PASSWORDLESS=true auto-logs-in the one admin so you’re not typing a password to talk to your own box.
  • History is bounded so a long chat doesn’t become a four-figure invoice. Each chat caps outbound history (100,000 tokens by default). The earliest system message stays sticky and the current turn stays pinned; older history gets added newest-first until the next whole message unit would blow the cap. Whole units only, so a tool result never gets orphaned from the call that produced it. And the composer shows you the real next-turn selection: sticky system, retained history, current draft, tokens used and free, and which complete turns got dropped. No guessing what you’re actually paying to send.

Every provider you’ve got, in one list

This is the bit I’d actually lead a demo with. CHATZ_UPSTREAMS is a JSON array, and you cram as many providers into it as you want, all live at the same time. Ollama running local models on your own box. aigate, your own gateway fronting whatever you host. Anthropic. z.ai. Any OpenAI-compatible endpoint you can reach. Chatz hits each one’s models endpoint, discovers what it actually serves, and merges the whole lot into a single picker.
So you open the model dropdown and your local llama is sitting right next to Claude, next to whatever your gateway fronts, and picking one routes that turn straight back to the upstream it came from. Every upstream names its own driver (openai, which also covers OpenAI-compatible gateways, or anthropic) and its own key by env name, never inline, and each driver is walled off from the others’ credentials so one provider’s key can’t leak into another’s calls. Friendly aliases ride on top without ever replacing the real model ID, and reasoning controls grey out for models that don’t advertise support, so the picker can’t offer you a knob that does nothing.

A demo mode for when the model won’t cooperate on camera

Small thing that shows someone’s actually tried to record this stuff: showcase mode. Live-model demos go off-script the second the camera’s on, so you flip CHATZ_SHOWCASE_MODE=true and bring the stack up. It keeps the real model list, MCP setup and chat behavior, but intercepts specific catalog prompts with deterministic thinking, synthetic tool activity, and then the embedded dashboards, with every displayed metric grounded in the visible synthetic tool results, not made up. The replies persist like normal chats, so a recording can refresh or keep going. It’s the difference between demoing generative UI and praying the model draws the chart you need on take four.


It’s early, v0.7.9 and moving fast. But the core bet is already there and it’s a good one: a self-hosted chat where the assistant renders live, real components instead of describing them, shipped as one small hardened container (a single Go binary with the UI baked in) plus a database you choose and nothing else. Point it at your OpenAI-compatible or Anthropic endpoints, wire up your MCP tools, and let the model draw you a dashboard that isn’t a lie.
github.com/psyb0t/chatz