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-relay

The 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-relay

Each 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:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerESA

Persistent 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/ws

Use wss:// for relays behind TLS, ws:// for unencrypted connections.

Environment variable reference

VariableTypeDefaultDescription
KUTL_RELAY_HOSTstring0.0.0.0Bind address
KUTL_RELAY_PORTu169100Bind port
KUTL_RELAY_NAMEstringkutl-relay-devHuman-readable relay name (sent in handshake)
KUTL_RELAY_REQUIRE_AUTHbooltrueRequire valid auth token in handshake
KUTL_RELAY_OUTBOUND_CAPACITYusize64Per-connection outbound channel capacity. Slow subscribers are evicted when their channel is full.
KUTL_RELAY_DATA_DIRpathDirectory for persistent data (registries, document state). Omit for ephemeral in-memory operation.
KUTL_RELAY_EXTERNAL_URLURLPublic URL of this relay. Used for device auth flow. Falls back to http://<host>:<port>
KUTL_RELAY_AUTHORIZED_KEYS_FILEpathFile of authorized DIDs (one per line). Authorized DIDs can subscribe to any space.
KUTL_RELAY_SNIPPET_MAX_DOC_CHARSusize10000Max document size for snippet extraction. Set to 0 to disable.
KUTL_RELAY_SNIPPET_DEBOUNCE_MSu642000Debounce 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.