Server Configuration¶
File: configs/server.yaml
Command: kenzy-server [config_path]
The server is the central WebSocket hub. It accepts connections from room nodes, runs the STT → LLM → TTS pipeline, and streams audio responses back. Each downstream service is optional — omit its url to disable that stage.
Full reference¶
| Key | Default | Description |
|---|---|---|
host |
"0.0.0.0" |
Bind address. 0.0.0.0 listens on all interfaces. |
port |
8765 |
WebSocket port |
tls.cert / tls.key |
— | Optional TLS: paths to a certificate + private key on the server host. When both are set the node WebSocket port speaks wss and the dashboard https (one cert pair covers both). See TLS below. |
log_level |
"info" |
Log verbosity |
experimental |
false |
Opt this server into experimental features that aren't ready to ship officially (none gated yet — reserved for future previews). Also switches the dashboard favicon to the experimental mark — gold tile, petrol "K", corner badge dot — so the browser tab is distinguishable from production at a glance. Editable from the dashboard's Settings tab. |
Discovery and config-pull¶
| Key | Default | Description |
|---|---|---|
discovery.enabled |
true |
Advertise the server as _kenzy._tcp over mDNS so nodes auto-discover it without a hardcoded server_url |
discovery.instance |
"kenzy-server" |
mDNS instance name |
discovery.token |
(generated by kenzy-init) |
Shared secret required in each node's hello (mismatching nodes are rejected) and the service-to-service bearer. kenzy-init generates one by default and matches it in node.yaml + .env (KENZY_SERVICE_TOKEN); the dashboard shows it under Settings for copy-paste. Clear it to allow unauthenticated joins. |
node_defaults |
{} |
Node tuning defaults (wake-word thresholds, VAD timing) pushed to every node on connect. Per-node overrides live in configs/nodes/<node_id>.yaml and shallow-merge over these. |
On connect, a node's hello carries its stable node_id and its room name; the server replies with the node's effective config = node_defaults merged with configs/nodes/<node_id>.yaml. The node blocks until this first frame arrives, then builds its audio stack from it (so hardware keys — audio device, sample rates, wakeword models, sounds — apply on that first pull); a later hardware change takes effect on restart, while live-tunable keys (thresholds, VAD timing, log levels) and the room name apply immediately. The per-node file is keyed by node_id, so a node keeps its config even if its room is renamed; pre-existing room-named files migrate automatically on first connect. This is how a room device runs with a bootstrap-only local file — see Node Configuration.
Central config for backend services¶
The server is also the config authority for the backend HTTP services. It exposes an always-on endpoint GET /config/<service> on the node WebSocket port (it runs whenever the server runs, independent of the dashboard), returning that service's effective config = the packaged default deep-merged with the server-owned override at configs/services/<service>.yaml. Secret-like keys are stripped, so secrets never leave the server — they stay in each host's environment / .env.
At boot, kenzy-stt/kenzy-tts/kenzy-llm/kenzy-speaker discover the server the same way a node does (mDNS, or an explicit KENZY_SERVER_URL), pull their config from this endpoint, and block with retry/backoff until the server answers — so the server must come up first (set After=kenzy-server in systemd units; the installer does this). The endpoint is gated by the service-to-service bearer (discovery.token / KENZY_SERVICE_TOKEN) when one is set. Each service also exposes a token-protected POST /restart that re-execs it to re-pull fresh config. Passing an explicit config path to a service (e.g. kenzy-stt configs/stt.yaml) bypasses the pull and loads locally — a dev/offline escape hatch.
Edit it all from the dashboard's Services tab: it reads each service's secret-stripped effective config, writes your changes to configs/services/<service>.yaml on the server, and restarts the service to apply. Secrets stay in the service host's environment and are never shown or stored.
Announce endpoint¶
The server exposes an always-on GET /announce on the node WebSocket port so external automations (e.g. Home Assistant) and scripts can make Kenzy speak in your rooms:
GET http://<server>:8765/announce?text=Dinner%20is%20ready&rooms=kitchen,office
Authorization: Bearer <discovery.token / KENZY_SERVICE_TOKEN>
text is required; rooms is an optional comma-separated list of room names (omit for every room). Returns {"announced": <node count>, …}. It must be a GET with query parameters (the websockets HTTP hook only accepts GET and exposes no request body), gated by the service-to-service bearer when one is configured.
For a ready-to-use Home Assistant rest_command, see Home Assistant Integration → Calling Kenzy from Home Assistant.
TLS (optional)¶
Kenzy runs in plaintext by default — normal for a trusted home LAN. The
installer offers to set this all up for you (say yes at the TLS
question, or pass --tls); to do it by hand, give the server a certificate and key:
One pair covers both listeners: the node WebSocket port becomes wss and the dashboard
becomes https (the login cookie is then marked Secure). A self-signed certificate
is fine — generate one with:
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout kenzy.key -out kenzy.crt -subj "/CN=kenzy"
Kenzy's own clients are built for exactly this posture: nodes and the backend services
connect encrypted but unverified by default, so nothing needs a CA installed —
traffic is protected from passive eavesdropping without pretending a trust chain exists.
mDNS advertises the TLS flag, so auto-discovering nodes and services switch to wss://
automatically; nodes with an explicit server_url need it changed to wss:// by hand.
Browsers do verify, so the dashboard shows a one-time certificate warning (or install
the cert on your machines).
The backend services are covered too: co-located services receive the server's cert
pair through the config they already pull and bring their own listeners up as https —
the whole mesh (pipeline calls, dashboard proxies, health checks) is then encrypted with
no extra setup. Services on other hosts supply their own pair via KENZY_TLS_CERT /
KENZY_TLS_KEY in their environment.
Operators with a real CA can turn verification on at each client: tls_verify: true /
tls_ca: in node.yaml (see Node Configuration), or KENZY_TLS_VERIFY=1 /
KENZY_TLS_CA=/path in a backend service's environment. The tls keys are deliberately
not dashboard-editable (a bad path would lock the dashboard out); manage them in
server.yaml. Everything also still works behind a reverse proxy terminating TLS — the
dashboard honors X-Forwarded-Proto.
Dashboard¶
Web fleet manager served by kenzy-server. On by default in the shipped config (set enabled: false to disable — nothing is then wired up: no route, no overhead). When enabled it provides a live fleet/health view, a per-node config editor (with room rename), node controls (trigger/stop/restart), TTS announcements, a log viewer, and a settings page. See the Dashboard guide for the full walkthrough.
| Key | Default | Description |
|---|---|---|
dashboard.enabled |
true (shipped config; false when the key is absent) |
Master switch. false ⇒ nothing below is mounted. |
dashboard.bind |
"0.0.0.0" |
Listener address. 0.0.0.0 is reachable across your LAN (the default — change the default password!); use 127.0.0.1 to restrict it to the server itself. Never port-forward it (plaintext HTTP) |
dashboard.port |
8770 |
Dashboard HTTP port (separate from the node WS port) |
dashboard.auth.username / dashboard.auth.password_hash |
admin / (hash of password) |
Browser login. Change it with the server-only kenzy-passwd CLI (or the dashboard's Settings page); never edit the hash by hand. |
dashboard.auth_token |
null |
Optional bearer token for API/CLI clients (the browser uses the login cookie, not this) |
dashboard.controls |
true |
Enable mutating actions — config edits, room rename, trigger/stop/restart, announcements. Set false for a read-only dashboard. |
dashboard.logs |
true |
Enable the pull-based log viewer (server, services, and per-node buffers) and the Activity tab. Set false to keep no logs/transcripts in memory. |
dashboard.allowed_hosts |
[] |
Optional list of hostnames the dashboard will accept in the Host header (DNS-rebinding defense). Empty = no Host restriction; the cross-site Origin check always applies. Set it when serving under a fixed name (e.g. ["kenzy.local"]). |
Keep the dashboard off the public internet
Login runs over plaintext HTTP on a LAN bind and defaults to admin / password. Bind it to localhost or the LAN only, change the password with kenzy-passwd, and do not port-forward the dashboard port.
STT service¶
| Key | Default | Description |
|---|---|---|
stt.url |
— | URL of the kenzy-stt /transcribe endpoint. Omit or set to null to skip transcription. |
stt.timeout |
60.0 |
HTTP timeout in seconds |
Speaker identification service¶
| Key | Default | Description |
|---|---|---|
speaker.url |
— | URL of the kenzy-speaker /identify endpoint. Omit to disable speaker ID. |
speaker.timeout |
10.0 |
HTTP timeout in seconds |
dialog.max_turns |
6 |
Max consecutive follow-up turns Kenzy holds the floor for in a multi-turn dialog before auto-ending |
alarm.ring_repeats |
10 |
How many times a firing alarm re-rings before giving up (a wake word stops it sooner) |
alarm.ring_interval |
25 |
Seconds between alarm re-rings |
LLM service¶
| Key | Default | Description |
|---|---|---|
llm.url |
— | URL of the kenzy-llm /process endpoint. Omit to disable LLM processing. |
llm.timeout |
30.0 |
HTTP timeout in seconds |
TTS service¶
| Key | Default | Description |
|---|---|---|
tts.url |
— | URL of the kenzy-tts /speak endpoint. Omit to disable TTS. |
tts.timeout |
60.0 |
HTTP timeout in seconds |
tts.chunk_size |
4096 |
Bytes per PCM chunk streamed to the node. At 24 kHz int16 mono, 4096 bytes ≈ 85 ms of audio. |
Home Assistant / MQTT integration¶
Opt-in; nothing is wired (zero overhead) unless enabled. Requires the mqtt extra (pip install "kenzy[server,mqtt]"). See Integrations → Home Assistant for the full guide.
| Key | Default | Description |
|---|---|---|
integrations.mqtt.enabled |
false |
Publish node state/events to an MQTT broker via HA MQTT Discovery |
integrations.mqtt.host |
"127.0.0.1" |
Broker hostname |
integrations.mqtt.port |
1883 |
Broker port |
integrations.mqtt.base_topic |
"kenzy" |
Topic prefix for Kenzy's state/command topics |
integrations.mqtt.chimes |
{} |
Extra named chimes for the kenzy/chime topic — name → WAV path on the server host. Bundled sound names (doorbell.wav) work without an entry |
integrations.mqtt.discovery_prefix |
"homeassistant" |
Must match HA's MQTT discovery prefix |
integrations.mqtt.commands |
true |
Accept inbound commands (Trigger/Stop buttons, Mute switch, command topics). false = read-only |
Broker credentials come from the environment, never this file: KENZY_MQTT_USERNAME / KENZY_MQTT_PASSWORD.
Example¶
host: "0.0.0.0"
port: 8765
discovery:
enabled: true
instance: "kenzy-server"
# token: "change-me" # require this in every node's hello
node_defaults: # pushed to nodes on connect (config-pull)
wakeword_threshold: 0.5
silence_rms_threshold: 50
silence_ms: 400
dashboard:
enabled: true # false ⇒ nothing is wired up (zero overhead)
bind: "0.0.0.0" # LAN-reachable (change the default password!); 127.0.0.1 = this machine only
port: 8770
stt:
url: "http://127.0.0.1:8767/transcribe"
timeout: 60.0
speaker:
url: "http://127.0.0.1:8768/identify"
timeout: 10.0
llm:
url: "http://127.0.0.1:8766/process"
timeout: 30.0
tts:
url: "http://127.0.0.1:8769/speak"
timeout: 60.0
chunk_size: 4096
Disabling stages
You can run a partial pipeline for development. For example, omit llm.url and tts.url to transcribe audio and log the results without generating responses.