Self-Hosting a Supernode

Run your own supernode to host communities on infrastructure you control.

Self-Hosting a Supernode

A supernode is a persistent relay server that facilitates message delivery, MLS commit ordering, and encrypted message archival for your communities. Running your own supernode gives you full control over your community’s infrastructure.

What Is a Supernode?

The supernode is a headless Rust binary (static-supernode) that:

  • Relays MLS-encrypted messages between community members.
  • Orders MLS Commits to maintain consistent group encryption state.
  • Archives encrypted ciphertext so offline members can catch up on missed messages.
  • Bridges push notifications to mobile clients via APNs/FCM.

The supernode never decrypts message content. It handles opaque ciphertext — it cannot read, modify, or forge messages. By default, it also cannot see client IP addresses because all connections route through Iroh relays.

Running your own supernode means you control the infrastructure. You decide the server location, the resource limits, the privacy settings, and the update schedule. No third party is involved.

Prerequisites

  • A Linux server (Ubuntu 22.04+ or equivalent) with a public IP address or reachable from the internet.
  • At least 1 GB RAM and 1 CPU core (sufficient for communities up to ~500 members).
  • Docker and Docker Compose (for the Docker deployment path), or the Rust toolchain (1.75+) for building from source.
  • A domain name pointed at your server (optional but recommended for TLS).
  • UDP port 4433 open in your firewall for QUIC connections (or whichever port you configure).

Quick Start with Docker Compose

The fastest way to run a supernode is with Docker Compose.

Create a directory for your supernode:

mkdir -p ~/supernode && cd ~/supernode

Create a supernode.toml configuration file:

[server]
listen_port = 4433
data_dir = "/data"

[privacy]
batch_interval_ms = 500
accept_direct_connections = false

[limits]
max_connections = 256
max_channels = 64
max_message_size = 262144

Create a docker-compose.yml:

version: "3.8"

services:
  supernode:
    image: ghcr.io/nicholasraimbault/static-supernode:latest
    container_name: static-supernode
    restart: unless-stopped
    ports:
      - "4433:4433/udp"
    volumes:
      - ./supernode.toml:/etc/supernode/supernode.toml:ro
      - supernode-data:/data
    command: ["--config", "/etc/supernode/supernode.toml", "--log-level", "info"]

volumes:
  supernode-data:

Start the supernode:

docker compose up -d

View logs to confirm it started:

docker compose logs -f supernode

You should see output like:

INFO starting static-supernode config_path="/etc/supernode/supernode.toml"
INFO configuration loaded listen_port=4433 max_connections=256
INFO loaded persistent secret key node_id=<your-node-id> path="/data/node.key"
INFO supernode NodeId (use this in client config) node_id=<your-node-id>
INFO supernode created, starting accept loop

The node_id printed in the logs is your supernode’s public identifier. You will need it to configure clients.

Building from Source

If you prefer to build and run the binary directly:

# Clone the repository
git clone https://github.com/nicholasraimbault/static.git
cd static

# Build the supernode binary (release mode)
cargo build --release -p static-supernode

# The binary is at target/release/static-supernode

Run it:

./target/release/static-supernode --config supernode.toml --log-level info

CLI Options

static-supernode [OPTIONS]

Options:
  -c, --config <PATH>       Path to configuration file [default: supernode.toml]
  -p, --port <PORT>         Override listen port from config file
      --log-level <LEVEL>   Log level: trace, debug, info, warn, error [default: info]
  -h, --help                Print help

The --port flag overrides the listen_port value from the configuration file, useful for testing or when the same config is shared across environments.

Configuration Reference

The supernode reads its configuration from a TOML file. All sections and fields are optional — missing values use sensible defaults.

[server] Section

FieldTypeDefaultDescription
listen_portu164433Port for QUIC connections. Clients connect to this port over UDP.
data_dirstring"./data"Directory for persistent data: the node secret key (node.key), logs, and future database files. Created automatically if it does not exist.
relay_urlstring (optional)nullCustom Iroh relay URL. When set, the supernode uses this relay instead of the default n0 production relays. Example: "http://100.78.233.128:3340".

[privacy] Section

FieldTypeDefaultDescription
batch_interval_msu64500Interval in milliseconds for batching and shuffling messages before delivery. Messages received within this window are shuffled and delivered together, preventing timing correlation. Set to 0 to disable batching (messages are delivered immediately).
accept_direct_connectionsboolfalseWhether to accept direct (non-relay) QUIC connections. When false (the default), all client connections must transit an Iroh relay, which hides client IP addresses from the supernode. Set to true only if you trust your network environment and want lower latency.

[limits] Section

FieldTypeDefaultDescription
max_connectionsusize256Maximum number of concurrent QUIC connections. Each connected client uses one connection. Raise this for larger communities.
max_channelsusize64Maximum number of channels that can be created across all communities hosted on this supernode.
max_message_sizeusize262144 (256 KB)Maximum size of a single message payload in bytes. Messages larger than this are rejected. This limit applies to the encrypted ciphertext, not the plaintext.

Full Example Configuration

[server]
listen_port = 4433
data_dir = "/var/lib/static-supernode"
# relay_url = "http://your-custom-relay:3340"

[privacy]
batch_interval_ms = 500
accept_direct_connections = false

[limits]
max_connections = 512
max_channels = 128
max_message_size = 524288

TLS Setup with Caddy

QUIC connections between clients and the supernode are already encrypted at the transport layer. However, if you want to front the supernode with a reverse proxy for domain-based routing or additional TLS termination, Caddy is the simplest option.

Create a Caddyfile:

supernode.yourdomain.com {
    reverse_proxy localhost:4433 {
        transport http {
            versions h3
        }
    }
}

Run Caddy:

sudo caddy run --config Caddyfile

Caddy automatically provisions TLS certificates via Let’s Encrypt. Clients can then connect to supernode.yourdomain.com instead of a raw IP address.

Note: Most deployments do not need a reverse proxy. The Iroh QUIC transport handles encryption natively. A reverse proxy is useful if you need domain-based routing or want to co-host the supernode alongside other services.

Firewall Rules

The supernode requires UDP port 4433 (or your configured port) to be open for inbound QUIC connections. If you are also running an Iroh relay, port 3340 (HTTP) is needed for relay coordination.

UFW (Ubuntu)

sudo ufw allow 4433/udp comment "Static supernode QUIC"
sudo ufw reload

iptables

sudo iptables -A INPUT -p udp --dport 4433 -j ACCEPT

Cloud Provider Firewalls

If you are running on AWS, GCP, DigitalOcean, or similar, configure the security group or firewall rules in the provider’s console to allow inbound UDP on port 4433.

Connecting Clients to Your Supernode

After your supernode is running, you need to configure clients to connect to it. The connection address is composed of:

  1. The supernode’s Node ID (printed in the logs on startup).
  2. The relay host through which the supernode is reachable.

The address format is:

<node_id>@<relay_host>

In the Static app:

  1. Open Settings.
  2. Under Network, enter your supernode address.
  3. The client will connect via the Iroh relay and authenticate.

When creating or joining a community, the supernode address is embedded in the invite code, so members joining via invite do not need to configure anything manually.

Persistent Identity

The supernode generates a persistent secret key on first run, stored at <data_dir>/node.key. This 32-byte key determines the supernode’s Node ID. The key file:

  • Is created with 0600 permissions (owner-only read/write) on Unix systems.
  • Must be preserved across restarts and upgrades for the Node ID to remain stable.
  • Should be backed up securely — if lost, the supernode gets a new identity and all clients must update their configuration.

Do not expose or share the node.key file. It is a secret key that allows impersonating your supernode.

Monitoring and Logs

The supernode uses the tracing framework with structured logging. Log levels:

LevelWhat it shows
errorUnrecoverable failures (crash-level events)
warnDegraded operation (connection drops, retry attempts)
infoLifecycle events (startup, shutdown, connections, channels created)
debugMessage flow details (message relayed, subscription changes)
traceWire-level data (CBOR payloads, raw packet sizes) — very verbose

For production, info is recommended. Use debug when investigating issues.

With Docker, view logs with:

docker compose logs -f --tail 100 supernode

For bare-metal deployments, consider piping output to a log file or using journalctl:

./static-supernode --config supernode.toml --log-level info 2>&1 | tee /var/log/static-supernode.log

Or as a systemd service:

[Unit]
Description=Static Supernode
After=network.target

[Service]
Type=simple
User=static
ExecStart=/usr/local/bin/static-supernode --config /etc/static/supernode.toml --log-level info
Restart=on-failure
RestartSec=5
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Updating

To update the supernode:

Docker:

docker compose pull
docker compose up -d

From source:

git pull
cargo build --release -p static-supernode
# Restart the service
sudo systemctl restart static-supernode

The supernode’s persistent identity (node.key) and data directory are preserved across updates. Clients do not need to reconfigure unless the Node ID changes (which only happens if node.key is deleted).

Security Hardening

For production deployments, consider these hardening measures:

  1. Run as a non-root user. Create a dedicated static system user with minimal permissions.

  2. Restrict file permissions. The data_dir should be owned by the supernode user with 0700 permissions. The node.key file should be 0600.

  3. Keep accept_direct_connections set to false. This ensures all client connections transit relays, so the supernode never learns client IP addresses.

  4. Set conservative resource limits. The max_connections, max_channels, and max_message_size limits protect against resource exhaustion. Size them for your expected community size, not for the maximum possible.

  5. Use a firewall. Only expose the QUIC port. Block all other inbound traffic.

  6. Enable automatic updates for the host operating system to patch security vulnerabilities.

  7. Monitor disk usage. The encrypted message archive grows over time. Plan for periodic cleanup or set archival limits.

  8. Raise the file descriptor limit. The supernode attempts to raise the fd limit to 65,535 on startup. Ensure the system limit (/etc/security/limits.conf or systemd LimitNOFILE) allows this for high connection counts.

  9. Backup node.key. Store a copy of the secret key in a secure offline location. If the server is lost and the key is not backed up, you will need to distribute a new supernode address to all clients.

Troubleshooting

Supernode starts but clients cannot connect:

  • Verify UDP port 4433 is open in your firewall and security group.
  • Check that the Node ID in client configuration matches the one printed in supernode logs.
  • If using a custom relay, verify the relay URL is reachable from both the supernode and clients.

“failed to read node.key” error:

  • Check file permissions on the data directory. The supernode user must have read/write access.
  • If the file is corrupted (wrong size), delete it and restart — a new key will be generated, but clients will need the new Node ID.

High memory usage with many connections:

  • Reduce max_connections in the config.
  • Each QUIC connection consumes memory for buffers and TLS state. For 256 connections, expect ~200-400 MB of RAM usage.

Messages not being delivered:

  • Check max_message_size. If encrypted messages exceed this limit, they are silently rejected.
  • Enable debug logging to see message flow details.