Nodofi Docs

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

  1. 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).
  2. Double-click run.bat to try it in that window (closing the window stops it), or install-startup.bat to also start it automatically whenever you sign in.
  3. 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 computerOn a domain (e.g. mainnet.yourdomain.com)
UptimeOnly while your computer is on and awake24/7/365 — a server doesn't sleep, close its lid, or lose power with your house
ReachabilityNeeds port forwarding, or a relay, to be reached from outside your own networkA real public address, no relay required
Good forTrying it out, a personal catalog, testingA node people actually rely on — including a company running one as real infrastructure, not a hobby machine
CostFree (your existing computer)A small VPS (a few dollars/month) or your own server
Note: Both are the exact same software — nothing to reconfigure to move from one to the other besides where it runs and, optionally, its Public address setting on the admin page.

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:

TypeNameValue
Amainnetyour 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).

Warning: Once a proxy is handling 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

  1. Copy the node key from the top of the admin page — paste it into Studio's "Send to Node…" dialog when publishing.
  2. 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.
  3. (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.
  4. (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.

Note: None of this needs the reverse-proxy config to change. See the /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;
    }
}
Note: 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

SettingDefaultWhere to set it
PORT8800Environment variable (systemd: Environment=PORT=8801, then daemon-reload + restart)
HOST0.0.0.0Environment variable — set to 127.0.0.1 when it's only ever reached through a local reverse proxy
NODE_DATA_DIR~/.nodofi-nodeEnvironment variable
Operator name, description, public address—Admin page, Identity & registry
Commission rate, payout wallet, submission feeOff (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 configuredAdmin page, Remote access — see above

Troubleshooting

Not reachable from outside

  1. Confirm DNS has propagated: dig +short mainnet.yourdomain.com should return your server's IP.
  2. Confirm the firewall/security group allows inbound 80 and 443.
  3. Confirm the proxy is actually running: sudo systemctl status apache2 (or nginx).
  4. curl -i https://mainnet.yourdomain.com/api/ping should 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.