Running a Nodofi Relay
A relay lets a node with no domain and no port forwarding — running from home, behind a router, even behind CGNAT — be reached anyway. It hosts nothing and stores essentially nothing: it just connects a node and whoever's asking for it, and passes messages between them.
Overview
A node connects outbound to a relay (which works through almost any router or firewall, the same way opening a website in a browser does), and anyone else can then reach that node by its permanent, cryptographic ID through the relay — no matter where the node's computer physically is or what its address is right now.
You need a relay if you're running a node locally without port forwarding. You generally don't need one if your node already has a real domain (see Setting up a node on a domain) — a relay exists specifically for the case a direct address doesn't.
Anyone can run a relay — it costs real bandwidth but no storage, and you can optionally charge nodes a periodic USDC fee to use yours (see Paid access), or leave it free.
Requirements
- Node.js 18 or later.
- Linux (systemd) for the packaged installer; Windows via Task Scheduler; any OS from source.
- A real, internet-reachable address if you want anyone other than yourself to use it — a relay running on your own home network alongside the node you're trying to make reachable defeats the purpose.
Quickstart
Windows
- Download nodofi-relay-win-x64.zip and extract it anywhere you'll keep it.
- Double-click
run.batto try it, orinstall-startup.batfor it to start automatically on sign-in. - Check http://localhost:8820 — you should see a small JSON status response.
Linux
tar -xzf nodofi-relay-linux-x64.tar.gz
cd nodofi-relay-linux-x64
sudo ./install.sh
Installs it to /opt/nodofi-relay, its config to ~/.nodofi-relay/.env, and
registers it as a systemd service (nodofi-relay).
sudo systemctl status nodofi-relay
curl -s http://localhost:8820/
By default a freshly installed relay is completely open: free, unregistered, no identity verification. That's fine for testing, but for a relay other people will actually rely on, see Setting up on a domain and Paid access & directory below.
Setting up on a domain
The same shape as a node's own domain setup: a small server, a DNS record, the relay running as a systemd service, and a reverse proxy handling TLS and — critically for a relay, whose entire job is a live socket.io connection — the WebSocket upgrade.
- Get a server and point DNS at it (e.g. an
Arecord forrelaypointing at its IP, giving yourelay.yourdomain.com) — identical to a node's own steps 1–2. - Install Node.js and the relay, same as the Linux quickstart above.
- Put a reverse proxy in front of it — see below for full Apache and Nginx configs.
- Configure it — edit
~/.nodofi-relay/.env:
thenRELAY_PUBLIC_URL=https://relay.yourdomain.com RELAY_NAME=My Relay RELAY_OPERATOR=Your name or company REGISTRY_URL=https://registry.nodofi.com # Optional: charge nodes for access # ACCESS_FEE_AMOUNT=2 # ACCESS_FEE_WALLET=04...your MooChedda wallet... # ACCESS_FEE_PERIOD_DAYS=30sudo systemctl restart nodofi-relay.RELAY_PUBLIC_URLis what gets listed in the relay directory and what the Registry live-probes to confirm the relay is real — it must be the address others can actually reach, not a local one. - Verify with the checks in Troubleshooting below.
Reverse proxy (Apache / Nginx)
A relay is nothing but a live socket.io connection, so getting the WebSocket upgrade right matters
even more here than for a node. Replace relay.yourdomain.com with your own subdomain.
Apache
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite
sudo systemctl reload apache2
<VirtualHost *:443>
ServerName relay.yourdomain.com
# ... your SSL/TLS config (or let certbot manage this block for you) ...
ProxyPreserveHost On
ProxyRequests Off
ProxyTimeout 86400
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/(.*) ws://127.0.0.1:8820/$1 [P,L]
ProxyPass / http://127.0.0.1:8820/
ProxyPassReverse / http://127.0.0.1:8820/
</VirtualHost>
The RewriteCond/RewriteRule pair is the important part: it intercepts only
requests carrying an Upgrade: websocket header and sends those through
mod_proxy_wstunnel (ws://); everything else — including the initial engine.io
HTTP handshake — falls through to the plain ProxyPass line below it. Without that split,
Apache proxies the HTTP parts fine (a plain health-check request looks completely healthy) but never
completes the WebSocket upgrade itself — which is exactly what a stuck "websocket error" on a
connecting node usually means. See Troubleshooting for how to tell the
two apart.
Nginx
server {
listen 80;
server_name relay.yourdomain.com;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
server_name relay.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/relay.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relay.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8820;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}
Nginx can handle both the plain HTTP and the WebSocket upgrade in one location block
(unlike Apache, which needs the split above) — the
Upgrade/Connection headers are what make that work. The
proxy_read_timeout matters too: nginx's 60-second default will silently drop an idle
socket.io connection, which looks like random disconnects rather than an obvious error.
Paid access & directory
Running a relay costs real bandwidth. Two optional, independent things you can turn on once
REGISTRY_URL is set:
Charge for access
Set ACCESS_FEE_AMOUNT, ACCESS_FEE_WALLET (your own MooChedda wallet) and
ACCESS_FEE_PERIOD_DAYS in .env. A node then pays from its own admin page
(Reachability panel) — the relay checks the payment against MooChedda's real ledger, not just
a claim, before letting that node's identity JOIN. Requesters (people just fetching an
app) are never charged — only nodes claiming a permanent identity are.
List it in the directory
Set RELAY_PUBLIC_URL, RELAY_NAME and RELAY_OPERATOR and your
relay appears on the public relay directory so node operators looking for one can
actually find yours. Every field shown there is re-confirmed by the Registry probing
RELAY_PUBLIC_URL itself, on a schedule — never just what was submitted at registration.
Full design details (why access is checked this way, exactly what's verified) are in the relay's own
README.md in the source.
Troubleshooting: "websocket error" / a node won’t connect
If a node's admin page shows "Not connected: websocket error" when pointed at your relay, work through these in order, from any machine other than the relay's own box:
1. Is the relay itself up?
curl -i https://relay.yourdomain.com/
Expect 200 and a small JSON body ({"ok":true,"service":"Nodofi Relay",...}).
If this fails, the relay process or DNS is the problem, not the proxy — check
sudo systemctl status nodofi-relay and DNS first.
2. Does the engine.io HTTP handshake work?
curl -i "https://relay.yourdomain.com/socket.io/?EIO=4&transport=polling"
Expect 200 and a body starting 0{"sid":".... If step 1 works but this
doesn't, the proxy isn't forwarding to the right port/path.
3. Does the WebSocket upgrade actually complete?
curl --http1.1 -i -N \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
"https://relay.yourdomain.com/socket.io/?EIO=4&transport=websocket"
Expect 101 Switching Protocols. If steps 1 and 2 succeed but this one comes
back 503, 404, or just hangs — that's the exact signature of a reverse proxy
that handles plain HTTP fine but was never configured to forward the WebSocket upgrade. Apply
the Apache or Nginx config above (specifically the
mod_proxy_wstunnel/RewriteRule split for Apache, or the
Upgrade/Connection headers for Nginx) and re-run this check — it should come
back 101 once it's wired up correctly, and a connecting node picks up the fixed connection
on its own next automatic retry (every couple of seconds), no restart needed on the node's side.
503 here that shows an Apache or Nginx error page (not JSON from the
relay itself) confirms the proxy is what's rejecting the upgrade — the relay's own socket.io server
never even saw the request.Uninstalling
Linux: sudo ./uninstall.sh from the folder you extracted (your .env is
kept). Windows: double-click uninstall-startup.bat, then delete the extracted folder.