Self-Hosting a Relay
The relay is a single binary with no required dependencies. All configuration is through environment variables. This page covers three deployment modes, from quick local development to production.
Development (ephemeral, no auth)
The fastest way to get a relay running. All state is in memory — stopping the relay loses everything. Authentication is disabled, so any client can connect.
KUTL_RELAY_REQUIRE_AUTH=false kutl-relayThe relay listens on 0.0.0.0:9100 by default. Clients connect to ws://127.0.0.1:9100/ws. This is the default relay URL for kutl init and kutl join, so no --relay flag is needed during development.
Self-hosted with file-based ACL
For a team relay accessible over the network. Authentication is enabled, and access is controlled by a file listing allowed DIDs (one per line). Data persists to disk.
export KUTL_RELAY_HOST=0.0.0.0
export KUTL_RELAY_PORT=9100
export KUTL_RELAY_EXTERNAL_URL=https://relay.example.com
export KUTL_RELAY_AUTHORIZED_KEYS_FILE=/etc/kutl/authorized_dids.txt
export KUTL_RELAY_DATA_DIR=/var/lib/kutl
kutl-relayEach team member runs kutl config show to find their DID, and you add it to the authorized keys file. Changes to the file take effect on the next connection. No relay restart required.
Example authorized_dids.txt
# Alice
did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK
# Bob
did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerESAPersistent storage
When KUTL_RELAY_DATA_DIR is set, the relay persists document state and space registrations to disk using SQLite. The registry database tracks space names, document UUIDs, and lifecycle state. Document content is stored alongside it. Without a data directory, all state lives in memory and the relay reconstructs everything from client operations when they reconnect.
Persistent storage is useful if you want the relay to survive restarts without clients needing to re-sync from scratch. The SQLite database requires no setup; the relay creates it automatically on first start.
TLS termination
The relay binary does not handle TLS directly. For production, place it behind a reverse proxy (nginx, Caddy, etc.) that terminates TLS and forwards WebSocket connections. Set KUTL_RELAY_EXTERNAL_URL to the public https:// URL so the relay can generate correct URLs for the device auth flow.
Example Caddy reverse proxy
relay.example.com {
reverse_proxy localhost:9100
}Connecting clients
Once the relay is running, clients connect by specifying the relay URL:
# Initialize a new space
kutl init --name my-project --relay wss://relay.example.com/ws
# Join an existing space
kutl join my-project --relay wss://relay.example.com/wsUse wss:// for relays behind TLS, ws:// for unencrypted connections.
Environment variable reference
| Variable | Type | Default | Description |
|---|---|---|---|
KUTL_RELAY_HOST | string | 0.0.0.0 | Bind address |
KUTL_RELAY_PORT | u16 | 9100 | Bind port |
KUTL_RELAY_NAME | string | kutl-relay-dev | Human-readable relay name (sent in handshake) |
KUTL_RELAY_REQUIRE_AUTH | bool | true | Require valid auth token in handshake |
KUTL_RELAY_OUTBOUND_CAPACITY | usize | 64 | Per-connection outbound channel capacity. Slow subscribers are evicted when their channel is full. |
KUTL_RELAY_DATA_DIR | path | — | Directory for persistent data (registries, document state). Omit for ephemeral in-memory operation. |
KUTL_RELAY_EXTERNAL_URL | URL | — | Public URL of this relay. Used for device auth flow. Falls back to http://<host>:<port> |
KUTL_RELAY_AUTHORIZED_KEYS_FILE | path | — | File of authorized DIDs (one per line). Authorized DIDs can subscribe to any space. |
KUTL_RELAY_SNIPPET_MAX_DOC_CHARS | usize | 10000 | Max document size for snippet extraction. Set to 0 to disable. |
KUTL_RELAY_SNIPPET_DEBOUNCE_MS | u64 | 2000 | Debounce delay (ms) for snippet computation after edits |
Don't want to run a relay?
kutlhub.com provides a managed relay with durable storage, a browser editor, and team features. See the Getting Started guide for the self-hosted path.