Skip to content

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.

Deploy guideDownload
zidane server start
zidane-serverwss://box.example.com

Desktop appSettings, Hosts

zidane
Rate limiting /chatbox
How is /chat rate limited?
    • The limiter in src/limits.ts keeps a sliding window per IP: 20 requests a minute on /chat, then a 429.

      It lives in memory, so each instance counts on its own. Behind a load balancer, move the counts to Redis.

      Opus 5.52s2$0.035.2k138

    Terminalzidane --host

    zidane
    The Zidane TUI attached to the server, on the same session: the prompt, a grep and a file read, then the answer.

    BrowsercreateServerHost

    app.example.com
    Sessions

    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.

    Sessions2 running
    New chat

    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.

    zidane-serverRun finished0:3818.0k tokens
    Rate limit the chat endpointbox
    Rate limit the chat endpoint: 20 requests a minute per IP, and tell clients how long to wait when it trips. The chart shows who hit it last night.
      • The chat route now takes a token from a per-IP bucket: 20 a minute, with bursts of 5. Over the limit it answers 429 with a Retry-After header, so clients know how long to wait.

        All 44 server tests pass.

        Opus 5.538s4$0.0717k1.1k

      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.

      Terminal
      # 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 start

      A 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.

      Terminal
      # 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 -d

      A 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.

      Connecting a host
      App.tsx
      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.

      wss://zidane.example.com
      // 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.

      Terminal
      # 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 zidane

      Deploy your server.

      The guide covers Docker, the release binary, TLS, tokens and Coolify, and the reference covers the WebSocket client.

      Other ways to use Zidane.