Skip to content

Configuration

Signal Fish Server uses a JSON config file with environment variable overrides.

Config File

On startup, the server looks for config.json in the working directory.

See config.example.json for a local-development reference, and the table below for all available overrides.

Essential Settings

Port

JSON
{
  "port": 3536
}

Environment override:

Bash
SIGNAL_FISH__PORT=8080 cargo run

Max Players

JSON
{
  "server": {
    "default_max_players": 8
  }
}

Environment override:

Bash
SIGNAL_FISH__SERVER__DEFAULT_MAX_PLAYERS=16 cargo run

Room Limits

JSON
{
  "server": {
    "max_rooms_per_game": 1000,
    "empty_room_timeout": 300,
    "inactive_room_timeout": 3600
  }
}
  • max_rooms_per_game - Maximum concurrent rooms per game name
  • empty_room_timeout - Seconds before an empty room is cleaned up (default: 300)
  • inactive_room_timeout - Seconds before an inactive room is removed (default: 3600)

Reconnection

JSON
{
  "server": {
    "enable_reconnection": true,
    "reconnection_window": 300,
    "event_buffer_size": 100
  }
}
  • enable_reconnection - Enable token-based reconnection (default: true)
  • reconnection_window - Seconds a reconnection token stays valid (default: 300)
  • event_buffer_size - Max events buffered for replay (default: 100)

Environment Variable Format

All config fields use the SIGNAL_FISH__ prefix. Nested fields use double underscores (__).

Examples:

Bash
# Top-level field
SIGNAL_FISH__PORT=3536

# Nested field: server.default_max_players
SIGNAL_FISH__SERVER__DEFAULT_MAX_PLAYERS=8

# Nested field: rate_limit.max_room_creations
SIGNAL_FISH__RATE_LIMIT__MAX_ROOM_CREATIONS=10

Configuration Reference

Complete reference of all configuration options with environment variable overrides:

Environment Variable Config Path Default Description
SIGNAL_FISH__PORT port 3536 Server listen port
SIGNAL_FISH__SERVER__DEFAULT_MAX_PLAYERS server.default_max_players 8 Default max players per room
SIGNAL_FISH__SERVER__PING_TIMEOUT server.ping_timeout 30 Seconds before a silent client is dropped
SIGNAL_FISH__SERVER__ROOM_CLEANUP_INTERVAL server.room_cleanup_interval 60 Seconds between room cleanup sweeps (must be > 0)
SIGNAL_FISH__SERVER__DRAIN_GRACE_SECS server.drain_grace_secs 30 Seconds between shutdown drain start and forced close 4000; 0 closes immediately
SIGNAL_FISH__SERVER__MAX_ROOMS_PER_GAME server.max_rooms_per_game 1000 Max rooms allowed per game name
SIGNAL_FISH__SERVER__EMPTY_ROOM_TIMEOUT server.empty_room_timeout 300 Seconds before an empty room is removed
SIGNAL_FISH__SERVER__INACTIVE_ROOM_TIMEOUT server.inactive_room_timeout 3600 Seconds before an inactive room is removed
SIGNAL_FISH__SERVER__RECONNECTION_WINDOW server.reconnection_window 300 Seconds a reconnection token stays valid
SIGNAL_FISH__SERVER__EVENT_BUFFER_SIZE server.event_buffer_size 100 Max events buffered for reconnection replay
SIGNAL_FISH__SERVER__ENABLE_RECONNECTION server.enable_reconnection true Enable reconnection support
SIGNAL_FISH__SERVER__HEARTBEAT_THROTTLE_SECS server.heartbeat_throttle_secs 30 Min seconds between last_seen heartbeat writes
SIGNAL_FISH__SERVER__REGION_ID server.region_id default Deployment region identifier; recorded in internal player and room state (not serialized to clients)
SIGNAL_FISH__SERVER__ROOM_CODE_PREFIX server.room_code_prefix null Optional prefix for generated room codes
SIGNAL_FISH__RATE_LIMIT__MAX_ROOM_CREATIONS rate_limit.max_room_creations 5 Max room creations per player per window
SIGNAL_FISH__RATE_LIMIT__TIME_WINDOW rate_limit.time_window 60 Rate limit window in seconds (must be > 0)
SIGNAL_FISH__RATE_LIMIT__MAX_JOIN_ATTEMPTS rate_limit.max_join_attempts 20 Max join attempts per player per window
SIGNAL_FISH__RATE_LIMIT__MAX_SIGNALS rate_limit.max_signals 600 Max validated WebRTC Signal dispatch attempts per player per window
SIGNAL_FISH__RATE_LIMIT__MAX_SIGNAL_ERRORS rate_limit.max_signal_errors 60 Max rejected WebRTC signal attempts per player per window
SIGNAL_FISH__PROTOCOL__MAX_GAME_NAME_LENGTH protocol.max_game_name_length 64 Max characters in a game name
SIGNAL_FISH__PROTOCOL__ROOM_CODE_LENGTH protocol.room_code_length 6 Length of generated room codes
SIGNAL_FISH__PROTOCOL__MAX_PLAYER_NAME_LENGTH protocol.max_player_name_length 32 Max characters in a player name
SIGNAL_FISH__PROTOCOL__MAX_PLAYERS_LIMIT protocol.max_players_limit 100 Hard ceiling on players per room
SIGNAL_FISH__PROTOCOL__ENABLE_MESSAGE_PACK_GAME_DATA protocol.enable_message_pack_game_data true Enable MessagePack game-data frames
SIGNAL_FISH__PROTOCOL__MIN_PROTOCOL_VERSION protocol.min_protocol_version 2 Lowest accepted protocol version
SIGNAL_FISH__PROTOCOL__MAX_PROTOCOL_VERSION protocol.max_protocol_version 3 Highest negotiated protocol version (clamp back to 2 to disable v3 features)
SIGNAL_FISH__PROTOCOL__SDK_COMPATIBILITY__ENFORCE protocol.sdk_compatibility.enforce false Enforce SDK platform/version checks (opt-in: the default platform list would otherwise reject unregistered/custom clients)
SIGNAL_FISH__PROTOCOL__SDK_COMPATIBILITY__MINIMUM_VERSIONS protocol.sdk_compatibility.minimum_versions platform defaults JSON object of minimum SDK versions
SIGNAL_FISH__PROTOCOL__SDK_COMPATIBILITY__RECOMMENDED_VERSIONS protocol.sdk_compatibility.recommended_versions platform defaults JSON object of recommended SDK versions
SIGNAL_FISH__PROTOCOL__SDK_COMPATIBILITY__CAPABILITIES protocol.sdk_compatibility.capabilities platform defaults JSON object of advertised capability lists
SIGNAL_FISH__PROTOCOL__SDK_COMPATIBILITY__NOTES protocol.sdk_compatibility.notes platform defaults JSON object of platform-specific notes
SIGNAL_FISH__PROTOCOL__PLAYER_NAME_VALIDATION__ALLOW_UNICODE_ALPHANUMERIC protocol.player_name_validation.allow_unicode_alphanumeric true Allow Unicode alphanumeric player names
SIGNAL_FISH__PROTOCOL__PLAYER_NAME_VALIDATION__ALLOW_SPACES protocol.player_name_validation.allow_spaces true Allow internal spaces in player names
SIGNAL_FISH__PROTOCOL__PLAYER_NAME_VALIDATION__ALLOW_LEADING_TRAILING_WHITESPACE protocol.player_name_validation.allow_leading_trailing_whitespace false Allow leading or trailing whitespace in player names
SIGNAL_FISH__PROTOCOL__PLAYER_NAME_VALIDATION__ALLOWED_SYMBOLS protocol.player_name_validation.allowed_symbols ["-","_"] Symbol allowlist for player names
SIGNAL_FISH__PROTOCOL__PLAYER_NAME_VALIDATION__ADDITIONAL_ALLOWED_CHARACTERS protocol.player_name_validation.additional_allowed_characters null Optional extra player-name characters
SIGNAL_FISH__LOGGING__DIR logging.dir logs Directory for rolling log files
SIGNAL_FISH__LOGGING__FILENAME logging.filename server.log Rolling log filename
SIGNAL_FISH__LOGGING__ROTATION logging.rotation daily File rotation policy (daily, hourly, never)
SIGNAL_FISH__LOGGING__LEVEL logging.level null Log level override (trace, debug, info, warn, error)
SIGNAL_FISH__LOGGING__ENABLE_FILE_LOGGING logging.enable_file_logging true Enable rolling file logs
SIGNAL_FISH__LOGGING__FORMAT logging.format json Log output format (json or text)
SIGNAL_FISH__SECURITY__CORS_ORIGINS security.cors_origins http://localhost:3000,http://localhost:5173 Allowed CORS origins (comma-separated or *)
SIGNAL_FISH__SECURITY__REQUIRE_WEBSOCKET_AUTH security.require_websocket_auth true Require app authentication on WebSocket connect
SIGNAL_FISH__SECURITY__REQUIRE_METRICS_AUTH security.require_metrics_auth true Require auth token for metrics endpoints
SIGNAL_FISH__SECURITY__METRICS_AUTH_TOKEN security.metrics_auth_token null Bearer token for metrics endpoints
SIGNAL_FISH__SECURITY__MAX_MESSAGE_SIZE security.max_message_size 65536 Max WebSocket message size in bytes
SIGNAL_FISH__SECURITY__MAX_SIGNAL_BYTES security.max_signal_bytes 16384 Max serialized size in bytes of a v3 Signal payload (must be > 0 and ≤ max_message_size)
SIGNAL_FISH__SECURITY__MAX_CONNECTIONS_PER_IP security.max_connections_per_ip 24 Max concurrent connections from one IP (covers a 16-player NAT/LAN session plus spectators and reconnect churn)
SIGNAL_FISH__SECURITY__TRANSPORT__TLS__ENABLED security.transport.tls.enabled false Enable built-in TLS listener
SIGNAL_FISH__SECURITY__TRANSPORT__TLS__CERTIFICATE_PATH security.transport.tls.certificate_path null Path to PEM certificate chain
SIGNAL_FISH__SECURITY__TRANSPORT__TLS__PRIVATE_KEY_PATH security.transport.tls.private_key_path null Path to PEM private key
SIGNAL_FISH__SECURITY__TRANSPORT__TLS__CLIENT_CA_CERT_PATH security.transport.tls.client_ca_cert_path null Path to trusted client CA bundle
SIGNAL_FISH__SECURITY__TRANSPORT__TLS__CLIENT_AUTH security.transport.tls.client_auth none TLS client auth mode (none, optional, require)
SIGNAL_FISH__SECURITY__TRANSPORT__TOKEN_BINDING__ENABLED security.transport.token_binding.enabled false Enable token-binding negotiation
SIGNAL_FISH__SECURITY__TRANSPORT__TOKEN_BINDING__REQUIRED security.transport.token_binding.required false Require token-binding subprotocol
SIGNAL_FISH__SECURITY__TRANSPORT__TOKEN_BINDING__REQUIRE_CLIENT_FINGERPRINT security.transport.token_binding.require_client_fingerprint false Require mTLS fingerprint in token-bound frames
SIGNAL_FISH__SECURITY__TRANSPORT__TOKEN_BINDING__SUBPROTOCOL security.transport.token_binding.subprotocol signalfish.tokenbinding.v1 Token-binding WebSocket subprotocol
SIGNAL_FISH__SECURITY__TRANSPORT__TOKEN_BINDING__SCHEME security.transport.token_binding.scheme sec_websocket_key_sha256 Token-binding signing scheme
SIGNAL_FISH__SECURITY__AUTHORIZED_APPS security.authorized_apps [] JSON array of authorized app entries
SIGNAL_FISH__AUTH__RATE_LIMIT_CACHE_CLEANUP_INTERVAL_SECS auth.rate_limit_cache_cleanup_interval_secs 300 Auth rate-limit cache cleanup interval
SIGNAL_FISH__AUTH__RATE_LIMIT_CACHE_RETENTION_SECS auth.rate_limit_cache_retention_secs 172800 Auth rate-limit cache retention window
SIGNAL_FISH__AUTH__RATE_LIMIT_CACHE_ALERT_ROWS auth.rate_limit_cache_alert_rows 100000 Auth rate-limit cache warning threshold
SIGNAL_FISH__COORDINATION__DEDUP_CACHE__CAPACITY coordination.dedup_cache.capacity 100000 In-memory coordinator dedup cache capacity (future-backend seam; not cross-process coordination)
SIGNAL_FISH__COORDINATION__DEDUP_CACHE__TTL_SECS coordination.dedup_cache.ttl_secs 60 In-memory coordinator dedup cache TTL
SIGNAL_FISH__COORDINATION__DEDUP_CACHE__CLEANUP_INTERVAL_SECS coordination.dedup_cache.cleanup_interval_secs 30 In-memory coordinator dedup cache cleanup interval
SIGNAL_FISH__COORDINATION__MEMBERSHIP_SNAPSHOT_INTERVAL_SECS coordination.membership_snapshot_interval_secs 30 Reserved membership-snapshot seam; the shipped coordinator is process-local
SIGNAL_FISH__METRICS__DASHBOARD_CACHE_REFRESH_INTERVAL_SECS metrics.dashboard_cache_refresh_interval_secs 5 Dashboard metrics refresh interval
SIGNAL_FISH__METRICS__DASHBOARD_CACHE_TTL_SECS metrics.dashboard_cache_ttl_secs 30 Dashboard metrics cache TTL
SIGNAL_FISH__METRICS__DASHBOARD_CACHE_HISTORY_WINDOW_SECS metrics.dashboard_cache_history_window_secs 300 Dashboard history window
SIGNAL_FISH__METRICS__DASHBOARD_CACHE_HISTORY_FIELDS metrics.dashboard_cache_history_fields ["active_rooms","rooms_by_game","player_percentiles","game_percentiles"] Dashboard history fields
SIGNAL_FISH__RELAY_TYPES__DEFAULT_RELAY_TYPE relay_types.default_relay_type matchbox Default relay integration label
SIGNAL_FISH__RELAY_TYPES__GAME_RELAY_MAPPINGS relay_types.game_relay_mappings {} JSON object mapping game names to relay labels
SIGNAL_FISH__SESSION__DEFAULT_TOPOLOGY session.default_topology relay Preferred topology for unmapped games (relay, host, mesh)
SIGNAL_FISH__SESSION__GAME_TOPOLOGY_MAPPINGS session.game_topology_mappings {} JSON object mapping game names to topologies
SIGNAL_FISH__SESSION__ENABLE_WEBRTC session.enable_webrtc true Permit the WebRTC transport for mesh/host upgrades
SIGNAL_FISH__SESSION__ENABLE_DIRECT session.enable_direct true Permit the Direct (LAN/routable) transport for host upgrades
SIGNAL_FISH__SESSION__ENABLE_ICE_PREGATHER session.enable_ice_pregather true Surface the composed ICE list on RoomJoined/Reconnected for eligible v3 WebRTC clients
SIGNAL_FISH__SESSION__ICE_SERVERS session.ice_servers [] JSON array of static ICE servers advertised in a WebRTC plan
SIGNAL_FISH__TURN__ENABLED turn.enabled false Mint and advertise self-hosted TURN credentials
SIGNAL_FISH__TURN__STATIC_AUTH_SECRET turn.static_auth_secret "" coturn --static-auth-secret (server-only; never sent to clients)
SIGNAL_FISH__TURN__URLS turn.urls [] JSON array of TURN server URLs (e.g. turn:turn.example.com:3478)
SIGNAL_FISH__TURN__STUN_URLS turn.stun_urls ["stun:stun.l.google.com:19302"] JSON array of STUN URLs advertised on WebRTC plans
SIGNAL_FISH__TURN__CREDENTIAL_TTL_SECS turn.credential_ttl_secs 3600 Lifetime in seconds of a minted TURN credential
SIGNAL_FISH__WEBSOCKET__ENABLE_BATCHING websocket.enable_batching true Enable outbound message batching
SIGNAL_FISH__WEBSOCKET__BATCH_SIZE websocket.batch_size 10 Max messages per batch
SIGNAL_FISH__WEBSOCKET__BATCH_INTERVAL_MS websocket.batch_interval_ms 16 Batch flush interval in milliseconds (must be > 0 when enable_batching is true)
SIGNAL_FISH__WEBSOCKET__AUTH_TIMEOUT_SECS websocket.auth_timeout_secs 10 Seconds to wait for auth after connect
SIGNAL_FISH__WEBSOCKET__IDLE_TIMEOUT_SECS websocket.idle_timeout_secs 300 Seconds without any inbound frame before an authenticated connection is closed (0 disables)
SIGNAL_FISH__WEBSOCKET__SERVER_PING_INTERVAL_SECS websocket.server_ping_interval_secs 10 Cadence for server-initiated RFC 6455 Ping frames (0 disables; must be ≤ 3600)
SIGNAL_FISH__WEBSOCKET__PONG_TIMEOUT_SECS websocket.pong_timeout_secs 5 Seconds allowed for the matching Pong before close 4003 activity_timeout (must be > 0 and ≤ 3600)
SIGNAL_FISH__WEBSOCKET__SEND_QUEUE_CAPACITY websocket.send_queue_capacity 1024 Per-connection data queue capacity (must be ≥ 1); only reliable delivery waits when full
SIGNAL_FISH__WEBSOCKET__CONTROL_QUEUE_CAPACITY websocket.control_queue_capacity 128 Per-connection v3 priority control queue capacity (must be ≥ 2)
SIGNAL_FISH__WEBSOCKET__SLOW_CONSUMER_TIMEOUT_MS websocket.slow_consumer_timeout_ms 5000 Milliseconds reliable delivery may wait for data-queue space before closing the recipient with 4002 slow_consumer (must be > 0 and ≤ 600000)
SIGNAL_FISH__WEBSOCKET__MAX_SOJOURN_MS websocket.max_sojourn_ms 15000 Maximum age of the oldest outbound item through socket-write completion before a 4002 slow_consumer close (must be > 0 and exceed batch_interval_ms when batching is enabled)
SIGNAL_FISH__WEBSOCKET__DELIVERY_STATS_INTERVAL_SECS websocket.delivery_stats_interval_secs 0 Seconds between v3 aggregate RelayStats and counter-only DeliveryReport snapshots (0 disables periodic snapshots, not exact gap reports; must be ≤ 3600)
RUST_LOG -- info Standard tracing log filter used when logging.level is null

Common Configurations

Development

JSON
{
  "port": 3536,
  "server": {
    "default_max_players": 8,
    "enable_reconnection": true
  },
  "logging": {
    "enable_file_logging": false
  },
  "security": {
    "cors_origins": "*",
    "require_websocket_auth": false
  }
}

Production

JSON
{
  "port": 3536,
  "server": {
    "default_max_players": 8,
    "empty_room_timeout": 180,
    "inactive_room_timeout": 1800
  },
  "rate_limit": {
    "max_room_creations": 5,
    "time_window": 60,
    "max_join_attempts": 20,
    "max_signals": 600,
    "max_signal_errors": 60
  },
  "logging": {
    "enable_file_logging": true,
    "rotation": "daily"
  },
  "security": {
    "cors_origins": "https://yourgame.com",
    "require_websocket_auth": true,
    "max_connections_per_ip": 24
  }
}

Rate Limiting

JSON
{
  "rate_limit": {
    "max_room_creations": 5,
    "time_window": 60,
    "max_join_attempts": 20,
    "max_signals": 600,
    "max_signal_errors": 60
  }
}
  • max_room_creations - Max room creations per player per time window
  • max_join_attempts - Max room join attempts per player per time window
  • max_signals - Max validated WebRTC Signal dispatch attempts per player per time window
  • max_signal_errors - Max rejected WebRTC signal attempts per player per time window
  • time_window - Rate limit window in seconds

Protocol Settings

JSON
{
  "protocol": {
    "max_game_name_length": 64,
    "room_code_length": 6,
    "max_player_name_length": 32,
    "max_players_limit": 100,
    "enable_message_pack_game_data": true
  }
}

WebSocket Settings

JSON
{
  "websocket": {
    "enable_batching": true,
    "batch_size": 10,
    "batch_interval_ms": 16,
    "auth_timeout_secs": 10,
    "idle_timeout_secs": 300,
    "server_ping_interval_secs": 10,
    "pong_timeout_secs": 5,
    "send_queue_capacity": 1024,
    "control_queue_capacity": 128,
    "slow_consumer_timeout_ms": 5000,
    "max_sojourn_ms": 15000,
    "delivery_stats_interval_secs": 0
  }
}
  • enable_batching - Batch outbound messages for better throughput
  • batch_size - Max messages per batch
  • batch_interval_ms - Batch flush interval
  • auth_timeout_secs - Seconds to wait for auth after connect
  • idle_timeout_secs - Post-authentication idle timeout (default: 300; 0 disables). An authenticated connection that produces no inbound WebSocket frame of any kind (including Ping/Pong) for this long receives a CONNECTION_IDLE_TIMEOUT error and is closed through the normal disconnect path, so the reconnection grace period still applies. The error is delivered on the connection's own outbound channel, so it reaches the client even though the server.ping_timeout state reaper (default 30s) has usually already removed a silent client's server-side registration by then. Clients that heartbeat (which server.ping_timeout already requires) are never affected by the 300s default. Keep this enabled in production — it reclaims zombie sockets that would otherwise hold file descriptors open indefinitely.
  • server_ping_interval_secs / pong_timeout_secs - The server sends an RFC 6455 Ping every 10 seconds by default and requires its matching Pong within 5 seconds. The probe is written directly by the socket layer, outside the application data/control queues; a miss closes with 4003 activity_timeout. Set the interval to 0 to disable server probes. The Pong timeout must remain greater than 0; both fields are capped at 3600 seconds.
  • send_queue_capacity - Per-connection data queue capacity in messages (default: 1024; must be ≥ 1). Reliable messages wait for space and apply sender backpressure. V3 latest and volatile messages never wait: their class policy accounts for any omission in a prior exact DeliveryReport. Larger values absorb bigger relay bursts; queue slots hold pointers, so the configured capacity costs little until messages actually queue.
  • control_queue_capacity - Per-connection control-plane queue capacity in messages (default: 128; must be ≥ 2). Within the active recipient generation, negotiated v3 drains this dedicated lane strictly before data, keeping exact delivery reports, peer lifecycle events, errors, and heartbeats from starving behind game data. The recipient's own room/spectator transitions are generation barriers that drain old-room data first. If exact accountability cannot be queued, the connection fails closed before later data is exposed.
  • slow_consumer_timeout_ms - How long (milliseconds) delivery may wait for reliable space in a full data queue before the recipient is disconnected as a slow consumer (default: 5000; must be > 0 and ≤ 600000). A connection that cannot absorb reliable traffic for this long is closed with authoritative WebSocket code 4002 slow_consumer; the SLOW_CONSUMER error is best effort. latest and volatile do not use this wait.
  • max_sojourn_ms - Maximum age of the oldest outbound item before the connection is closed loudly with 4002 slow_consumer (default: 15000; must be > 0), even if it continues sending pings. The deadline covers every queue lane, the current batch/in-flight item, and completion of the current socket write. It must be greater than batch_interval_ms when batching is enabled, ensuring a healthy item gets a scheduled flush opportunity.
  • delivery_stats_interval_secs - Periodic v3 aggregate/counter snapshot cadence (default: 0, disabled; must be ≤ 3600). This does not suppress exact gap-bearing DeliveryReport frames, which are emitted whenever a lossy class or unsupported format omits a sequence range.

See Delivery semantics for class policies and the exact gap-authorization contract.

Session Topology (Protocol v3)

JSON
{
  "session": {
    "default_topology": "relay",
    "game_topology_mappings": {},
    "enable_webrtc": true,
    "enable_direct": true,
    "enable_ice_pregather": true,
    "ice_servers": []
  }
}
  • default_topology - Preferred topology for games not in game_topology_mappings (relay, host, mesh; default: relay)
  • game_topology_mappings - Per-game topology overrides, e.g. {"FastFPS": "mesh", "BoardGame": "host"}
  • enable_webrtc - Permit the WebRTC transport for mesh/host upgrades (default: true)
  • enable_direct - Permit the Direct (LAN/routable) transport for host upgrades (default: true)
  • enable_ice_pregather - Surface the composed ICE list (static ice_servers, then STUN, then a freshly minted TURN credential) on RoomJoined/Reconnected so v3 WebRTC-capable clients can pre-gather ICE candidates during the lobby wait (default: true). Only fires for non-relay-desired games in non-finalized rooms; the SessionPlan ICE list supersedes it. Set false to mint TURN credentials only via SessionPlan emission (finalize / late-join / host-failover re-plan) (see TURN deployment)
  • ice_servers - Static ICE (STUN/TURN) servers advertised in a WebRTC SessionPlan and in the pre-gather list; appended before any TURN-derived entries. Every URL must use a stun:/stuns:/turn:/turns: scheme (validated at startup)

Every upgrade gracefully degrades to the relay floor, so a fully-disabled deployment keeps working exactly like v2. ICE servers are advertised only when a WebRTC topology (mesh, or host with the WebRTC transport) is actually selected — plus, with enable_ice_pregather, at join/reconnect time for v3 WebRTC-capable members of non-relay-desired games still in the lobby; under the default relay topology each v3 member receives an explicit no-peer relay/relay SessionPlan, while no ICE is pre-gathered at all. V2 members receive no plan.

TURN and STUN (ICE Credentials) (Protocol v3)

TURN is fully self-hosted: when enabled, the server self-mints short-lived coturn REST credentials for a TURN server you run. No third-party cloud is ever contacted and no external credentials are required.

JSON
{
  "turn": {
    "enabled": false,
    "static_auth_secret": "",
    "urls": [],
    "stun_urls": ["stun:stun.l.google.com:19302"],
    "credential_ttl_secs": 3600
  }
}
  • enabled - Mint and advertise self-hosted TURN credentials (default: false). When false, the block is inert and only stun_urls is advertised
  • static_auth_secret - coturn --static-auth-secret. Required when enabled
  • urls - TURN server URLs, e.g. ["turn:turn.example.com:3478"]. Required (non-empty) when enabled
  • stun_urls - Public STUN URLs advertised on WebRTC plans regardless of enabled (default: ["stun:stun.l.google.com:19302"])
  • credential_ttl_secs - Lifetime in seconds of a minted TURN credential. Must be > 0 when enabled (default: 3600)

Security: static_auth_secret is server-only

turn.static_auth_secret is a server-only secret and is never sent to clients — only the short-lived ephemeral username/credential pair derived from it ever reaches a client. It must match the value passed to coturn via --static-auth-secret (coturn --use-auth-secret). Prefer setting it via the environment variable SIGNAL_FISH__TURN__STATIC_AUTH_SECRET rather than checking it into the config file.

STUN phone-home note

turn.stun_urls defaults to a public Google STUN server (stun:stun.l.google.com:19302). It is only advertised to clients once a WebRTC topology (mesh, or host with the WebRTC transport) is actually selected — or, with session.enable_ice_pregather, when a v3 WebRTC-capable client joins a non-relay-desired game's lobby — it is never sent under the default relay topology. Operators who want no third-party STUN dependency should set stun_urls: [].

Validation

Validate your config without starting the server:

Bash
cargo run -- --validate-config

Print the resolved config (with environment overrides):

Bash
cargo run -- --print-config

Next Steps