Run your agents on your own server.
zidane-server keeps sessions, settings and provider keys on a machine you control. The desktop app, the TUI and browser pages follow its sessions live.
Desktop appSettings, Hosts
Terminalzidane --host
BrowsercreateServerHost
Built from zidane-ui and a captured TUI frame. All three clients show the same session.
One server, many clients.
Attach the TUI with zidane --host, add it in the desktop app's settings, or open a browser page built on the kit. With Remote hosts on (experimental), both apps stay connected to every saved server and group sessions by host, next to the local daemon.
Runs keep going when you close the app.
Runs live on the server: close the window mid-run and the next client picks up the session, still streaming. Questions and plans wait there for any client, and a restart aborts running turns, then waits up to 8 seconds for them to settle.
Tokens, TLS and origin checks.
The server listens on loopback until you give it TLS or a TLS proxy, and each connection needs a client token, stored as a SHA-256 hash and checked in constant time.
Browser origins start on an empty allowlist, rate limits cap messages, upgrades and connections per token and address, and an audit log records connections, token changes and refused upgrades.
# One token per device, printed once
zidane-server token create --name laptop
zidane-server token list # never shows secrets
zidane-server token revoke laptop # drops its connections
# Off loopback: TLS in the server, or a TLS proxy
zidane-server start --host 0.0.0.0 \
--tls-cert cert.pem --tls-key key.pem
# Browser pages connect only from the origins you list
ZIDANE_SERVER_ALLOWED_ORIGINS=https://app.example.com \
zidane-server startA Docker image, or one binary.
The image (linux/amd64, linux/arm64) keeps its data on a /data volume, Compose adds Caddy for HTTPS, and Coolify runs it behind Traefik. On a VPS, use the release binary: one self-contained executable for Linux and macOS.
# Docker: the data on a named volume, the port on loopback
docker run -d --name zidane --restart unless-stopped \
-v zidane-data:/data -p 127.0.0.1:4280:4280 \
ghcr.io/tahul/zidane-server:8.1
# Create the first token
docker exec zidane zidane-server token create --name laptop
# Or Compose, with Caddy for HTTPS on your domain
ZIDANE_SERVER_VERSION=8.1 ZIDANE_DOMAIN=zidane.example.com \
docker compose -f server/docker-compose.selfhost.yml \
--profile caddy up -dA browser client in a few lines.
createServerHost connects a web page to the server, and ChatProvider runs the kit's session list, transcript and composer on it. Allow the page's origin in ZIDANE_SERVER_ALLOWED_ORIGINS, give it its own token, and approve tool calls from the TUI or the desktop app.
import 'zidane-ui/styles.css'
import { createServerHost } from 'zidane-ui/adapters'
import {
ChatProvider,
HostComposer,
HostTranscript,
SessionList,
} from 'zidane-ui/chat'
import { useState } from 'react'
const host = createServerHost({
url: 'wss://zidane.example.com',
token: 'zds_your_web_token',
})
export function App() {
const [id, setId] = useState<string | null>(null)
return (
<ChatProvider host={host}>
<SessionList activeId={id} onSelect={setId} search />
{id && <HostTranscript sessionId={id} />}
{id && <HostComposer sessionId={id} />}
</ChatProvider>
)
}JSON-RPC over one WebSocket.
Clients open with ipc.hello, which checks the protocol version, then call JSON-RPC 2.0 methods such as sessions.list and chat.send, and the server pushes each change as an ipc.signal notification.
Browsers send their token in the zidane.bearer.<token> subprotocol, since they cannot set headers.
// The client opens with the handshake
{ "jsonrpc": "2.0", "id": 1, "method": "ipc.hello",
"params": { "protocolVersion": 13, "pid": 0,
"appVersion": "web" } }
// The server answers with its version and who it is
{ "jsonrpc": "2.0", "id": 1,
"result": { "protocolVersion": 13, "pid": 7,
"appVersion": "8.0.4", "startedAt": 1791626400000,
"hostKind": "server", "hostName": "box" } }
// Then it pushes each host event as an ipc.signal
{ "jsonrpc": "2.0", "method": "ipc.signal",
"params": { "kind": "run:start", "sessionId": "s_41f2",
"runId": "r_9c07", "$seq": 128 } }Health checks and logs.
GET /healthz answers on the server's port without a token, for load balancers and Coolify, and zidane-server health asks the running server over its unix socket, exiting 0 when healthy.
In a container, the server warns at start when /data would not survive a redeploy, and --verbose logs each connection and request.
# Liveness on the server's port: no token, no audit entry
curl -fsS https://zidane.example.com/healthz # ok
# The image's health check: the unix socket, or HTTP
docker exec zidane zidane-server health
docker exec zidane zidane-server health --http
# Start-up warnings, such as a /data that a redeploy empties
docker logs -f zidaneDeploy your server.
The guide covers Docker, the release binary, TLS, tokens and Coolify, and the reference covers the WebSocket client.