Running a Nodofi Node
A node hosts apps built in Nodofi Studio and serves them to anyone who opens one — and can earn its operator a commission on every sale of a priced app.
Overview
A Nodofi Node is a small Node.js application. It stores the apps it's been sent, serves them (over a
direct connection, or through a relay) to whoever asks — the App Browser, an embedded
<script src="/nodo.js"> on any web page, or Studio's own node picker — and, if its
operator chooses to accept priced apps, checks and records USDC payments made straight to that
operator's own MooChedda wallet.
Nothing about running a node requires trusting Nodofi Inc. with anything: it holds its own identity key, its own app files, and its own payout wallet, all on the machine it runs on.
Requirements
- Node.js 18 or later — the only real prerequisite. Everything else (the app's own dependencies) installs automatically on first run.
- Windows, Linux (any systemd-based distribution — Ubuntu, Debian, Fedora, and most others), or macOS (run from source; no packaged installer yet).
- To be reachable by anyone besides yourself: either a public domain pointed at the machine (this page), a router configured to forward its port, or a Relay.
Quickstart
The fastest way to try it, on your own computer:
Windows
- Download nodofi-node-win-x64.zip and extract it anywhere you'll keep it (not a temporary Downloads folder — it runs from where you put it).
- Double-click
run.batto try it in that window (closing the window stops it), orinstall-startup.batto also start it automatically whenever you sign in. - Open http://localhost:8800 in your browser.
Linux
tar -xzf nodofi-node-linux-x64.tar.gz
cd nodofi-node-linux-x64
sudo ./install.sh # installs Node.js dependencies, sets it up as a systemd service
Then open http://localhost:8800 in a browser on that machine.
macOS (or any OS, from source)
git clone <this repository>
cd desktop-node
npm install
npm start
Either way, the page at :8800 — the admin page — is where you copy your node's
key (paste it into Studio's "Send to Node…" dialog), set a payout wallet and commission if you want to
accept priced apps, and see what's currently hosted. It only ever works from the same machine the node
runs on (see below) — that's intentional, not a bug.
Local vs. a domain
There are two very different ways to run a node, and which one makes sense depends on what you want from it:
| On your own computer | On a domain (e.g. mainnet.yourdomain.com) | |
|---|---|---|
| Uptime | Only while your computer is on and awake | 24/7/365 — a server doesn't sleep, close its lid, or lose power with your house |
| Reachability | Needs port forwarding, or a relay, to be reached from outside your own network | A real public address, no relay required |
| Good for | Trying it out, a personal catalog, testing | A node people actually rely on — including a company running one as real infrastructure, not a hobby machine |
| Cost | Free (your existing computer) | A small VPS (a few dollars/month) or your own server |
Setting up on a domain
This is the recommended setup for anyone hosting a node people (or a company) actually depend on. The shape of it: a small Linux server, a DNS record pointing at it, Nodofi Node running as a systemd service, and a reverse proxy (Apache or Nginx) in front of it handling TLS and forwarding traffic in.
1. Get a server
Any small Linux VPS works — DigitalOcean, Linode, Hetzner, a AWS/GCP/Azure instance, or a company's own server. Ubuntu 22.04 LTS or later is a safe default. You don't need much: 1 CPU / 1–2GB RAM is plenty for a node.
2. Point DNS at it
In your domain's DNS settings, add an A record for the subdomain you want to use (e.g.
mainnet) pointing at the server's public IPv4 address:
| Type | Name | Value |
|---|---|---|
A | mainnet | your server's IP address |
That gives you mainnet.yourdomain.com. DNS changes can take anywhere from a few minutes
to a few hours to propagate.
3. Install Node.js and Nodofi Node
# Node.js 18+ via NodeSource
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
# Nodofi Node itself
tar -xzf nodofi-node-linux-x64.tar.gz
cd nodofi-node-linux-x64
sudo ./install.sh
This installs it to /opt/nodofi-node, its data to ~/.nodofi-node, and
registers it as a systemd service (nodofi-node) that starts on boot and restarts itself if
it ever crashes. Confirm it's running:
sudo systemctl status nodofi-node
curl -s http://localhost:8800/api/ping
4. Put a reverse proxy in front of it
The node listens on plain HTTP, port 8800, by default on every network interface. A
reverse proxy is what gets you HTTPS on the real domain, and — since a node uses a live socket.io
connection for direct app requests, not just plain HTTP — it needs the WebSocket-upgrade handshake
forwarded correctly, not just ordinary requests. See Reverse proxy below
for full Apache and Nginx configs (they're the same technique a
relay needs, since both run the same kind of server).
443 for you, close off direct access to
8800 from the internet — either with your firewall (ufw deny 8800, allowing it
only from 127.0.0.1), or by setting the node to only listen locally in the first place
(add Environment=HOST=127.0.0.1 to its systemd unit, then daemon-reload and
restart). Otherwise your node ends up reachable two ways — the proxied, encrypted
https://mainnet.yourdomain.com, and also a raw, unencrypted
http://<server IP>:8800 that bypasses the proxy entirely.5. Manage it remotely
The admin page (/, /admin.js, everything under /admin/)
deliberately refuses to load for anyone except the machine it's running on — even through your own
reverse proxy, on purpose, so nobody else can ever reach your node's settings by default. See
Managing it remotely below for the two ways to actually use it once your
node is on a server — an SSH tunnel, or a username/password the node itself gates (curl-friendly, no GUI
required on the server at all).
6. Finish setup
- Copy the node key from the top of the admin page — paste it into Studio's "Send to Node…" dialog when publishing.
- Set its Public address (under Identity & registry) to
https://mainnet.yourdomain.com— this is what gets told to the Registry and to anyone who asks how to reach this node directly. - (Optional) Register with a Registry — enter a registry address (Nodofi's own is
https://registry.nodofi.com) so your node gets a verifiable identity, a name people recognize, and a listing on the public node directory. With a real domain like this, you generally don't need a relay — that's specifically for nodes that can't get a direct address of their own. - (Optional) Accept priced apps — under Payments, set a MooChedda wallet and the commission you want to keep on each sale.
Managing it remotely
Two ways to reach a node's admin page once it's running on a server you're not sitting at:
An SSH tunnel
Forward the port instead of opening it publicly — works the moment the server has SSH, nothing to set up on the node's side at all:
ssh -L 8800:localhost:8800 you@mainnet.yourdomain.com
Then open http://localhost:8800 in a browser on your own computer — it now
tunnels straight to the node's admin page on the server, exactly as if you were sitting at it. Close the
terminal and the tunnel (and remote access) closes with it.
Username + password (no SSH needed)
For a server with no SSH access at all — a locked-down box, a host with only a web console, or one you
simply don't want to keep an SSH session open to — the node has its own login, gated by localOnly
until an account exists. A node's very first local visit
(http://localhost:8800, before it's ever moved to a server) redirects straight to
/admin/setup instead of the ordinary dashboard — there's no way to skip it, so setting up an
account is never something to remember to do later. Set a username and password there and you're done —
that alone is enough to sign in from anywhere, immediately, no extra confirmation step.
No GUI on the server at all? The setup is one plain curl call, run locally on the box —
this step can never be done remotely, on purpose, so nobody but whoever actually controls the machine can
ever claim the first admin account:
curl -s -X POST http://localhost:8800/admin/auth/setup \
-H "Content-Type: application/json" -H "X-Node-Admin: 1" \
-d '{"username":"YOUR_USERNAME","password":"a-real-password-at-least-12-characters"}'
Sign in right afterward at https://mainnet.yourdomain.com/admin/login with that username
and password — reachable from the whole internet once set up, so use a real password; the login itself is
rate-limited against repeated guessing (the first few mistakes are free, then an exponentially growing
lockout), but a weak password is still weak. The dashboard's own Remote access section can
rotate it for a new username and password any time (takes effect at once, no confirmation step —
a typo is trivially fixed by doing it again from the same access).
An authenticator app (optional, added after logging in)
A second factor on top of the username/password above, set up any time from the dashboard's
Remote access section (/admin/totp) once you're already signed in — locally, or
remotely with just the username and password. It's deliberately kept separate from the initial curl-only
setup, since scanning a QR code needs an ordinary browser: POST /admin/auth/totp/setup hands
back a secret and an otpauth:// URI (shown as a QR code on that page — scan it with Google
Authenticator, Authy, 1Password, or enter the secret by hand), then POST /admin/auth/totp/enable
{"otp":"123456"} confirms it with one real code from the app. Unlike the password, this does keep a
pending/confirm step — a mistyped or misscanned secret has no easy undo the way a wrong password does — so
username+password alone keeps working until you actually confirm a real code; a half-finished setup can
never lock you out. POST /admin/auth/totp/disable {"password":"..."} turns it back off (the
current password is required, independent of whatever session calls it).
Turning remote access off entirely
POST /admin/remote-access {"enabled":false} (also a button on the dashboard) switches
remote reachability off without touching the account, password or authenticator setup at all — the node
then answers a remote visitor exactly as if it had never been set up in the first place (no hint an
account even exists), until it's switched back on. This can be done from a remote session too —
an emergency lockdown reachable from your phone if you ever suspect something's wrong — but switching it
back on is deliberately local-only: once off, no session (even the one that just switched
it off) is honored for a non-local request any more, so re-enabling needs the same local/SSH access an
SSH-only setup always required.
/admin
note inside the Apache and Nginx configs below — do NOT add a blanket
"deny" rule on /admin at the proxy layer, or none of the above can ever be reached at all.
The node's own login is what decides who gets in; the proxy's only job is to forward the request.Ghost protection (optional, Beezifi Security)
A separate, optional layer specifically for /admin/login — the one page on a node
reachable from the whole internet once remote access is set up above. Turn it on from the dashboard's
Remote access section with an API key from your own Beezifi account, and every request to
/admin/login is checked against whatever rules you've configured there (IP/country
blocklists, bot detection, rate limits, geofencing) before this node's own rate limiting, password check
or TOTP ever run. A request Beezifi rejects gets back exactly the "ghost response" configured for it
there — a fake page, a redirect, or a plain block — with no hint a real login exists behind
it. Nothing else on the node is affected, on or off, and it fails closed (a 503) rather than
silently passing through if Beezifi itself can't be reached — without ever locking you out of the
node: local access, and any session you're already signed into remotely, are unaffected either way.
Reverse proxy (Apache / Nginx)
Both configs below terminate TLS on the standard ports and forward everything — including the
WebSocket upgrade a live app request needs — to the node at 127.0.0.1:8800. Replace
mainnet.yourdomain.com with your own subdomain.
Apache
Needs mod_proxy, mod_proxy_http, mod_proxy_wstunnel and
mod_rewrite:
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite
sudo systemctl reload apache2
Vhost (get a certificate first with certbot --apache, or fill in your own):
<VirtualHost *:443>
ServerName mainnet.yourdomain.com
# ... your SSL/TLS config (or let certbot manage this block for you) ...
ProxyPreserveHost On
ProxyRequests Off
ProxyTimeout 86400
# Do NOT block /admin here. It's tempting to add a blanket "Require all denied" on /admin as defense in
# depth, but the node's own remote-access login (username + password, optionally an authenticator code —
# see "Managing it remotely" above) depends on this proxy actually forwarding those requests: the app itself
# is what checks who's allowed in, using ProxyPreserveHost's Host header and the client IP it forwards
# (X-Forwarded-For, set automatically by mod_proxy). Blocking /admin at this layer doesn't add safety on
# top of that — it just breaks remote access outright, with no error that points back here.
# WebSocket upgrade (live app requests) -> mod_proxy_wstunnel
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/(.*) ws://127.0.0.1:8800/$1 [P,L]
# Everything else -> plain HTTP proxy
ProxyPass / http://127.0.0.1:8800/
ProxyPassReverse / http://127.0.0.1:8800/
</VirtualHost>
Nginx
server {
listen 80;
server_name mainnet.yourdomain.com;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
server_name mainnet.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mainnet.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mainnet.yourdomain.com/privkey.pem;
# Do NOT add a "deny all" location for /admin here, same reasoning as the Apache config above — the
# node's own remote-access login relies on this proxy actually forwarding those requests (with
# X-Forwarded-For/X-Forwarded-Proto set below, which is what the app uses to decide who's allowed in).
location / {
proxy_pass http://127.0.0.1:8800;
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;
# socket.io connections are long-lived; nginx's 60s default read timeout will silently drop idle
# ones otherwise, which looks like random disconnects rather than an obvious error.
proxy_read_timeout 86400;
}
}
certbot --nginx -d mainnet.yourdomain.com (or --apache
above) will get and auto-renew the certificate for you, and rewrites these configs to point at it —
easier than filling in the paths by hand.Reviewing submissions
Anyone can send this node an app with no key at all (Studio's "Send to Node…" dialog, leaving the Node key field blank — see Submitting without a key). It never goes live on its own: it's queued for you, the node's owner, to approve or reject from the admin page's Review queue section.
Each entry shows the app's name, description, publisher, size, whether it would replace an already-live app at that same UID, whether a submission fee was paid (see below), and — click into it — the actual files, so you can review real code before anything goes live. Approve runs it through the exact same publish path a trusted, keyed send uses. Reject asks for an optional reason, which matters more than it might look: a no-key submitter has no key or account on this node, so a typed-in reason is the only way they ever find out why — they can check back with:
curl -s https://mainnet.yourdomain.com/api/submissions/<uid>/status
# {"status":"pending","submittedAt":"…"} — still waiting on you
# {"status":"approved"} — live; fetch it the normal way
# {"status":"rejected","reason":"…","rejectedAt":"…"} — your reason, if you gave one
# {"status":"unknown"} — no submission on file for this uid
No key or account is needed for that check — a submission's uid is this node's usual
unguessable identifier, the same thing that already gates fetching a live app's own contents. Resubmitting
the same uid (fixing and re-sending the same app) clears any earlier rejection, so the
submitter isn't stuck seeing a stale verdict once they've addressed it.
Deterring spam with a submission fee
Under the admin page's Payments section, Submission fee sets a USDC amount a no-key submitter must pay, straight to your own MooChedda wallet, before an unfamiliar app is even queued for you to look at — a real, independently-checked transfer, not just a claim, so flooding your queue with junk actually costs something. It's not refunded on rejection, but resubmitting the same app after fixing it is free — only a genuinely different app needs a new payment. See MooChedda: Submission fees for exactly how that payment is verified.
Configuration reference
| Setting | Default | Where to set it |
|---|---|---|
PORT | 8800 | Environment variable (systemd: Environment=PORT=8801, then daemon-reload + restart) |
HOST | 0.0.0.0 | Environment variable — set to 127.0.0.1 when it's only ever reached through a local reverse proxy |
NODE_DATA_DIR | ~/.nodofi-node | Environment variable |
| Operator name, description, public address | — | Admin page, Identity & registry |
| Commission rate, payout wallet, submission fee | Off (free apps only) | Admin page, Payments |
| Registry address | — | Admin page, Identity & registry |
| Relay address | — | Admin page, Reachability |
| Remote access (username/password, authenticator app, on/off) | Off until first configured | Admin page, Remote access — see above |
Troubleshooting
Not reachable from outside
- Confirm DNS has propagated:
dig +short mainnet.yourdomain.comshould return your server's IP. - Confirm the firewall/security group allows inbound
80and443. - Confirm the proxy is actually running:
sudo systemctl status apache2(ornginx). curl -i https://mainnet.yourdomain.com/api/pingshould return{"ok":true,...}.
A direct app request fails with a WebSocket error
Same underlying cause as a relay's own WebSocket
troubleshooting — the proxy is passing plain requests through but not completing the WebSocket
upgrade. Follow that page's curl-based diagnosis (it applies here unchanged, just against your node's
own address and port 8800) and the reverse proxy configs
above.
The admin page shows "can only be opened on the computer that runs this node"
That's correct, expected behavior when you reach it through your public domain with no remote-access account set up (or with one switched off) — see Managing it remotely above for an SSH tunnel or a username/password login instead.
Remote login works but a reverse-proxy deploy of it doesn't
If curl -i https://mainnet.yourdomain.com/api/ping works but
/admin/login always 404s or hangs, check your reverse-proxy config for a blanket
deny/Require all denied rule on /admin — see the note inside the
Apache/Nginx configs above. That rule made sense before this node had its own
login, but it now blocks the very feature it's trying to protect.
Uninstalling
Linux: sudo ./uninstall.sh from the folder you extracted (removes the service; your
data in ~/.nodofi-node is kept). Windows: double-click uninstall-startup.bat,
then delete the extracted folder. Full detail in
desktop-node/INSTALL.md in the source.