# How ogulcancelik/herdr Server Client Architecture Works: A Deep Dive

> Explore the ogulcancelik/herdr server client architecture. Learn how this headless server manages state and communicates with clients using JSON-RPC and binary frame streaming.

- Repository: [Can Celik/herdr](https://github.com/ogulcancelik/herdr)
- Tags: deep-dive
- Published: 2026-05-31

---

**Herdr runs as a headless server process that owns all application state and communicates with thin terminal clients via two independent Unix-domain sockets—one for JSON-RPC control commands and another for high-throughput binary frame streaming.**

The ogulcancelik/herdr project implements a clean separation between UI rendering and application logic through its distinct server-client model. Unlike traditional terminal applications that bundle state management with the interface, herdr maintains a persistent headless server process while allowing multiple lightweight clients to attach and detach dynamically. This architecture enables remote control, session persistence, and rendering optimizations impossible in monolithic designs.

## The Dual-Socket Communication Model

Herdr utilizes two separate communication channels to isolate control-plane traffic from data-plane streaming. This separation ensures that API commands remain responsive even when the system streams large render payloads.

### The JSON-RPC API Socket (`herdr.sock`)

Defined in [`src/api/server.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/api/server.rs) and [`src/api/client.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/api/client.rs), this socket handles newline-delimited JSON requests for external tooling, CLI commands, and the auto-detect subsystem. It exposes control-plane operations, allowing scripts to query state or trigger actions without connecting to the high-throughput binary stream.

### The Binary Client Socket (`herdr-client.sock`)

The primary data plane operates over `herdr-client.sock`, implemented in [`src/server/client_transport.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/client_transport.rs) and [`src/client/mod.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/client/mod.rs). This channel uses length-prefixed bincode messages defined in [`src/protocol/wire.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/protocol/wire.rs) to stream rendered frames, input events, clipboard data, and notifications with minimal overhead.

## Connection Establishment and Protocol Handshake

When a client initiates a connection, it opens the client socket path returned by `crate::server::socket_paths::client_socket_path()` and transmits a `ClientMessage::Hello` payload containing critical metadata.

The handshake includes the protocol version, terminal geometry (columns and rows), cell dimensions in pixels, requested render encoding, key-binding profile, and launch mode:

```rust
// src/client/mod.rs
let hello = ClientMessage::Hello {
    version: PROTOCOL_VERSION,
    cols,
    rows,
    cell_width_px,
    cell_height_px,
    requested_encoding,
    keybindings: requested_keybindings(),
    launch_mode: if direct_attach_requested {
        ClientLaunchMode::TerminalAttach
    } else {
        ClientLaunchMode::App
    },
};
protocol::write_message(stream, &hello)?;

```

The server validates the version using `protocol::check_client_version()` and inspects custom keybindings before responding with `ServerMessage::Welcome`. If validation fails, the server sends an error string in the welcome message and immediately closes the connection:

```rust
// src/server/client_transport.rs
match protocol::check_client_version(version) {
    VersionCheck::Compatible => {}
    VersionCheck::Incompatible(reason) => {
        let welcome = ServerMessage::Welcome {
            version: PROTOCOL_VERSION,
            encoding: RenderEncoding::SemanticFrame,
            error: Some(reason),
        };
        protocol::write_message(&mut stream, &welcome)?;
        return Ok(());
    }
}

```

## Bidirectional Message Flow and Threading

After a successful handshake, the server spawns two dedicated threads per client to handle concurrent bidirectional traffic without blocking the main event loop.

### Server-to-Client Writer Thread

The writer thread manages the `ClientWriter` struct containing separate channels for control messages and render frames. It serializes data as length-prefixed binary blobs, prioritizing control traffic over render data:

```rust
// src/server/client_transport.rs
loop {
    // Priority: control > render
    if let Ok(ctrl) = control_rx.try_recv() {
        write_framed_bytes(&mut stream, &ctrl);
        continue;
    }
    if let Ok(rend) = render_rx.try_recv() {
        write_framed_bytes(&mut stream, &rend);
        let _ = server_event_tx.blocking_send(ServerEvent::ClientWriterDrained { client_id });
        continue;
    }
}

```

Control messages flow through an unbounded `std::sync::mpsc::Sender`, while render frames move via a single-slot `SyncSender` to prevent memory buildup during slow client consumption.

### Client-to-Server Reader Thread

The reader thread continuously parses incoming messages using the framing helpers from [`src/protocol/wire.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/protocol/wire.rs). It decodes `ClientMessage` variants (input, resize, clipboard, attach requests) and forwards them as `ServerEvent` variants on a Tokio `mpsc::Sender` for the central event loop to process.

## Render Encoding Negotiation

Herdr supports two render modes negotiated during the handshake and stored in `ClientState.render_encoding`:

- **SemanticFrame**: The server transmits full `FrameData` structures including cell attributes, cursor positions, and optional Kitty graphics. The client applies differential updates using `render_ansi::BlitEncoder` to minimize terminal traffic.
- **TerminalAnsi**: The server sends raw ANSI escape sequences via `TerminalFrame` structures, offering maximum bandwidth efficiency for terminals that natively parse ANSI streams.

## Socket Path Resolution and Session Management

The system resolves socket paths through the logic in [`src/server/socket_paths.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/socket_paths.rs), supporting multiple named sessions and environment overrides:

- **HERDR_SOCKET_PATH**: Overrides the API socket path and automatically derives the client socket name by appending `-client.sock`.
- **HERDR_CLIENT_SOCKET_PATH**: Serves as a legacy fallback for direct client socket configuration.
- **Default paths**: If no environment variables are set, sockets reside in the session's configuration directory.

```rust
// src/server/socket_paths.rs
pub fn client_socket_path() -> PathBuf {
    if crate::session::explicit_session_requested() {
        return crate::session::client_socket_path_for(crate::session::active_name().as_deref());
    }
    client_socket_path_from_overrides(
        std::env::var(crate::api::SOCKET_PATH_ENV_VAR).ok().as_deref(),
        std::env::var(CLIENT_SOCKET_PATH_ENV_VAR).ok().as_deref(),
    )
}

```

This design allows multiple isolated herdr sessions to coexist on the same machine without socket conflicts.

## Error Handling and Graceful Shutdown

The protocol implements clean disconnection semantics for both parties. Clients may send `ClientMessage::Detach` to request a controlled exit, while the server can broadcast `ServerMessage::ServerShutdown` with an optional reason string to force client termination.

All framing errors—including oversized frames, unexpected EOFs, or bincode deserialization failures—are wrapped in `protocol::FramingError` and trigger `ServerEvent::ClientDisconnected`, ensuring the server promptly cleans up resources for disconnected clients.

## Summary

- **ogulcancelik/herdr** implements a headless server architecture where all state lives in a persistent process separate from the terminal UI.
- Communication flows over two Unix-domain sockets: a JSON-RPC control socket (`herdr.sock`) and a binary data socket (`herdr-client.sock`).
- The handshake protocol in [`src/protocol/wire.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/protocol/wire.rs) negotiates version compatibility, render encodings (`SemanticFrame` or `TerminalAnsi`), and terminal geometry before admitting clients.
- Each client connection spawns dedicated reader and writer threads on the server, with prioritized control channels and backpressure-aware render queues.
- Socket paths resolve through environment variables (`HERDR_SOCKET_PATH`, `HERDR_CLIENT_SOCKET_PATH`) or session-specific directories, enabling multi-session workflows.

## Frequently Asked Questions

### How does herdr handle multiple clients connecting to the same server?

The server maintains independent reader and writer thread pairs for each connected client, as implemented in [`src/server/client_transport.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/server/client_transport.rs). Each client receives its own render stream and input channel, allowing multiple terminals to view or control the same session simultaneously, though the server state remains singular and shared.

### What happens if the client and server versions are incompatible?

During the initial handshake, the server calls `protocol::check_client_version()` to validate the client's protocol version. If versions mismatch, the server sends a `ServerMessage::Welcome` containing an error description and immediately closes the connection before entering the main message loop, preventing protocol confusion.

### Can external scripts control herdr without using the terminal client?

Yes. External tools connect to the API socket (`herdr.sock`) using the JSON-RPC protocol defined in `src/api/`. By constructing `Request` objects and utilizing `ApiClient::local()` (which respects the `HERDR_SOCKET_PATH` environment variable), scripts can execute commands, query state, and manage sessions without attaching a thin client.

### Which render encoding should I choose for remote connections?

For high-latency or bandwidth-constrained links, request `TerminalAnsi` encoding during the handshake, which transmits raw ANSI escape sequences rather than full frame structures. For local connections or terminals supporting advanced features like Kitty graphics, `SemanticFrame` provides richer metadata and allows the client to optimize differential updates.