Nodofi Docs

Using Nodofi Studio

Studio is where an app gets built, in the browser, and where it's sent out to a node to become reachable. This page covers the publishing side — creating a project, sending it to a node (with or without that node's key), and how anyone (including code you write yourself) actually requests an app afterward. For the editor itself (the workbench, the visual editor, scene apps, the AI assistant), see the main project README.md — this page focuses on what's specific to the decentralized network.

Overview

The shape of the whole system, end to end:

Nodofi Studio ──(you click "Send to Node…")──▶  a Node  ◀──requests for it──  anyone

Studio itself never hosts a finished app or holds anyone's money. It's a browser-based editor with a live preview; when you're ready, you send the built project straight to a node of your choosing — your own, a friend's, or any node you know the address (or public key) of. From that point on, the node is what actually serves the app to whoever asks for it; Studio's only remaining role is remembering where you last sent each project and, if the app is priced, verifying payments after the fact (see MooChedda payments).

Nothing about this requires a node you don't already trust: you choose which node(s) to send a project to, and a requester separately chooses which node(s) they trust to fetch from.

Requirements

  • Node.js 18 or later (node -v).
  • MariaDB 10.5 or newer (MySQL 8 is not tested).
  • A terminal. Don't use sudo to run Studio itself — it creates root-owned files that later stop it from running as your own user.

Quickstart

Running it locally, on your own computer, with no domain or reverse proxy:

# 1. Create a database user (as a MariaDB admin: mariadb -u root, or sudo mariadb on Linux)
CREATE USER 'nodofi'@'127.0.0.1' IDENTIFIED BY 'pick-a-strong-password';
GRANT ALL PRIVILEGES ON nodofi_studio.* TO 'nodofi'@'127.0.0.1';
GRANT CREATE ON *.* TO 'nodofi'@'127.0.0.1';   -- lets db:init create the database itself
FLUSH PRIVILEGES;

# 2. Install and configure
git clone <this repository>
cd BeezifiStudio
npm install
cp .env.example .env   # then edit DB_USER, DB_PASSWORD, DB_NAME and JWT_SECRET at least

# 3. Create the tables and start
npm run db:init
npm start

Open http://localhost:3000, register an account, and create a project. Re-run npm run db:init any time you update the code — it applies schema upgrades without touching existing data.

Note: JWT_SECRET signs sign-in tokens and, unless SECRETS_KEY is set separately, protects every user's saved Anthropic API key and Beezifi Storage credentials. Generate one with node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" and never change it later — changing it invalidates every existing session and makes previously-saved secrets undecryptable.

Deploying to production

Publishing works with zero extra setup — sites are served at http://127.0.0.1:3000/<app>/ by default. To serve real sites on a real domain (https://yourdomain.com/<app>/), and to let users connect a domain they own to a site they've published, set these in .env:

SettingWhat it's for
NODE_ENV=productionTurns on secure, __Host--prefixed session cookies, which require HTTPS. Also makes DOMAIN_VERIFICATION=off get ignored, if it's set — custom-domain DNS checks always run for real in production.
SITES_DOMAINThe public domain published apps live on — e.g. nodofi.com. Must be a different host name from Studio's own (STUDIO_URL): a published site's JavaScript then runs on a different browser origin than Studio itself, so it can never act as a signed-in Studio user — the API also refuses cross-origin writes, and the session cookie is host-only.
STUDIO_URLStudio's own public address, e.g. https://studio.nodofi.com.
TRUST_PROXY=1Set this when Studio runs behind any reverse proxy (Apache, Nginx, Caddy) — without it, Express reads the wrong host/protocol from requests and routing between Studio and published sites breaks.
SERVER_IPThis server's public IPv4 address — shown to users as the value for an apex-domain (@) A record when they connect their own domain.
CUSTOM_DOMAIN_TARGETThe host name a subdomain custom domain's CNAME should point to. Defaults to SITES_DOMAIN, which is correct for almost every setup — only set this if custom-domain traffic needs to land somewhere other than SITES_DOMAIN itself (e.g. a CDN in front of it).
DNS_SERVERSOptional, comma-separated (e.g. 8.8.8.8,8.8.4.4). Overrides the resolver custom-domain verification uses. See Troubleshooting below for why this matters.

A user connects their own domain from a project's Custom Domains… menu (or "Use your own domain…" after publishing): Studio shows a CNAME or A record to point at this server, plus a TXT record (_nodofi.<name> = nodofi-verify=<token>) proving ownership. Once both check out, the domain goes live — up to 5 per site, the first verified one is the main domain, and the platform address keeps working alongside it.

Reverse proxy & HTTPS

Studio itself only ever speaks plain HTTP on PORT (default 3000). Something needs to sit in front of it for HTTPS — and, because users can connect any domain they verify ownership of, that something also needs to obtain a certificate for each one automatically, with nobody manually running certbot per customer. Which approach fits depends on what's already running on the server.

Option A — Caddy (best for a dedicated box)

Caddy's on_demand_tls asks Studio whether a domain is verified before ever requesting a certificate for it, so certificates are only ever issued for real, verified customers — no per-domain config, no reloads, nothing to run on a schedule:

{
	on_demand_tls {
		ask http://localhost:3000/_internal/tls-ask
	}
}

yourdomain.com {
	reverse_proxy localhost:3000
}

studio.yourdomain.com {
	reverse_proxy localhost:3000
}

# every other host name (customers' own domains) — the app only says yes to verified ones
https:// {
	tls {
		on_demand
	}
	reverse_proxy localhost:3000
}
Note: This is the simplest option, but it means Caddy owns ports 80/443 for everything on that box. If the server already runs other sites under Apache or Nginx, switching all of it to Caddy is a much bigger change than it looks — see Option B.

Option B — Apache, with auto-issued certificates per domain

Apache has no built-in equivalent to Caddy's on_demand_tls — it can't ask Studio anything before deciding what certificate to present. This matters specifically for a server that already runs other sites under Apache (a very common shared-hosting setup): every domain still needs a real certificate, obtained the moment its owner verifies it in Studio, with zero manual steps and zero risk to whatever else is already running on that box.

Two scripts ship in scripts/production/ for exactly this:

  • provision-custom-domain.sh <domain> — for one domain: writes an Apache vhost, obtains a certificate with certbot certonly --webroot (never lets certbot guess at Apache config — the vhost it writes is exact, including the WebSocket-upgrade rule Studio's live preview needs), then rewrites the vhost to its final HTTPS form. Safe to re-run.
  • provision-custom-domains-cron.sh — reads Studio's own .env for database credentials, finds every domain with site_domains.status = 'active' that has no certificate yet, and provisions it.

Set up once:

sudo mkdir -p /var/www/certbot-webroot
sudo chown www-data:www-data /var/www/certbot-webroot

cd scripts/production
chmod +x provision-custom-domain.sh provision-custom-domains-cron.sh

sudo crontab -e
# add, with the real absolute path to this directory:
*/5 * * * * ADMIN_EMAIL=you@yourdomain.com /path/to/scripts/production/provision-custom-domains-cron.sh >> /var/log/nodofi-domain-provision.log 2>&1

Both scripts assume the app itself is reachable at 127.0.0.1:3501 — set APP_PORT when calling either one if Studio runs on a different port in your setup. To provision a single domain immediately instead of waiting for the next cron tick:

sudo ADMIN_EMAIL=you@yourdomain.com ./provision-custom-domain.sh customerdomain.com

Installation troubleshooting

A custom domain shows someone else's certificate

Symptom: the browser shows ERR_CERT_COMMON_NAME_INVALID, naming a completely unrelated domain as the certificate's actual owner. This means whatever's serving HTTPS on that IP has no certificate for the requested domain and fell back to whatever certificate it already had loaded for a different site on the same server. It is almost always one of:

  • The reverse proxy is Apache, not Caddy — /_internal/tls-ask only means anything to Caddy's on_demand_tls. See Option B above.
  • The domain was only just verified, and nothing has provisioned a certificate for it yet — wait for the next cron tick, or run provision-custom-domain.sh by hand.
  • apachectl -S shows no vhost at all for the domain — the provisioning script hasn't run successfully yet; check its cron log.

"Verify" fails even though the DNS record is definitely there

Domain verification's DNS lookups go through Node's own resolver, which by default follows this machine's configured DNS servers — those can lag behind a change, or hold a longer negative cache from before the record existed, than a public resolver would. Confirm the record is actually live from elsewhere first:

dig +short TXT _nodofi.yourdomain.com @8.8.8.8

If that shows the right value but Studio's own "Verify" still fails, set DNS_SERVERS (see Deploying to production above) to force verification lookups through a public resolver instead of whatever this machine defaults to, then try Verify again.

Confirming the auto-provisioning cron job is actually running

sudo crontab -l                                # the line is there, with a real absolute path?
systemctl status cron                          # the cron daemon itself is running?
grep CRON /var/log/syslog | tail -20           # cron is actually invoking it on schedule?
tail -f /var/log/nodofi-domain-provision.log   # what the script itself did on its last few runs

Creating an app

A project holds any number of .html, .css and .js files, edited with a live preview as you type. The one thing every app needs, so a node — or anyone else's page — can actually start it once its files arrive:

function NODO_START() {
  // your app's entry point — called once the app's HTML, CSS and JS
  // have all been loaded into the page (or an iframe — see Nodo.load below)
  document.body.innerHTML = '<h1>Hello from Nodofi</h1>';
}

Studio's editor warns you (in the "Send to Node…" dialog itself) if no file defines NODO_START — the app will still send and be reachable, but nothing will run automatically when it's requested.

Beyond that one requirement, everything else about building the app — the Monaco-powered editor with project-aware autocomplete, the visual point-and-click editor, live CSS/JS preview, Prettier formatting, and scene apps (multiple HTML files stitched into one page via SHOW_SCENE(...), the original Beezifi/Nodofi app style) — works exactly the same whether or not you ever send the result anywhere. See the main README.md for the full editor reference.

Sending to a node

Right-click a project → Send to Node…. The dialog asks for:

FieldWhat it's for
Node address (or ID)Where the app goes. A direct address (https://mainnet.yourdomain.com, or http://localhost:8800 for a node on your own computer), or a node's 32-character ID plus the registry it's registered with, for a node reachable only through a relay. See Finding & linking nodes below for picking one without typing either by hand.
Node key (optional)Shown at the top of that node's own admin page. With a key, the app goes live immediately. Left blank, it's submitted for the node owner's review instead.
List in the app directoryWhether this app shows up when someone searches that node's own directory (still fetchable by UID either way — see Requesting an app).
Icon, publisher, description, screenshotsShown wherever the app is listed — an App Browser, a node's own directory search.
Price (USDC)Optional. See MooChedda: Getting paid for the commission split and what "agreeing to a rate" locks in.

With a key, Studio talks to the node directly — your browser, not Studio's server — so a node on your own computer or LAN needs no port forwarding to receive it. Sending the same UID again updates it in place; any already-open page using watch: true (see below) reloads on its own.

The underlying API (for scripting a publish yourself)

Everything the dialog does is one plain HTTP call — useful if you'd rather push from a build script than click through Studio each time:

curl -s -X POST https://mainnet.yourdomain.com/api/apps \
  -H "Authorization: Bearer <node key>" -H "Content-Type: application/json" \
  -d '{
    "uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "name": "My App",
    "version": "1.0.0",
    "files": [
      { "type": "html", "name": "index.html", "content": "<h1>Hi</h1>" },
      { "type": "js",   "name": "app.js",     "content": "function NODO_START(){}" }
    ]
  }'

uid is a stable 32-character hex id you choose once per app (Studio generates and remembers one per project); sending the same uid again updates that app rather than creating a new one. A price and payout wallet can ride along too — see desktop-node/README.md for the full request shape.

Submitting without a key

Leave the Node key field blank and Send still works — the app is queued on that node for its owner to review, instead of going live right away. Nothing about it is reachable by anyone until they approve it (see Node: Reviewing submissions for that side of it).

Note: Not currently supported for a node with no direct address (relay-only, reached only by ID) — Studio will ask you to enter that node's key instead in that case. A direct address is required for the no-key path today.

If the node charges a submission fee

A node's owner can require a small USDC payment — set by them, on their own admin page, not by Nodofi — before an unfamiliar app is even queued for review. This is deliberate, and Studio explains it right in the dialog: without a real cost attached, anyone could flood a node's review queue with junk or malicious submissions for free. Because the fee is an independently-verified USDC transfer (never just a claim), spamming a node actually costs something — which is what keeps review queues usable and the wider network trustworthy for everyone publishing to it. It's paid straight to the node's own operator, not to Nodofi, and is not refunded if the app is rejected — but fixing and resubmitting the same app afterward is free. See MooChedda: Submission fees for exactly how the payment itself works.

Checking what happened to it

A no-key submitter has no account on that node, so there's no dashboard to check back on. Studio's "Send to Node…" dialog itself has a Check submission status button when reopened for a project last sent this way — or check directly, no auth needed at all:

curl -s https://mainnet.yourdomain.com/api/submissions/<uid>/status
# {"status":"pending", "submittedAt":"…"}
# {"status":"approved"}
# {"status":"rejected", "reason":"…", "rejectedAt":"…"}
# {"status":"unknown"}

Finding & linking nodes

The "Send to Node…" dialog's Browse nodes… button searches a Nodofi Registry (default https://registry.nodofi.com, or any other registry's address) and lists nodes registered there — name, operator, whether it has a direct address or is relay-only, its rating. Picking one fills in the dialog's address (and registry, for a relay-only pick) automatically.

You can also enter a node's address (or, for a relay-only node, its 32-character ID plus the registry it's registered with) by hand — the same two fields Browse nodes… fills in for you. A relay-only node's address looks different in Studio (it has no real URL of its own) but works exactly the same for sending and requesting once entered.

Requesting an app

However an app was sent, this is how any page — yours or anyone else's — actually loads and runs it, once you know its uid and which node to ask:

<script src="https://mainnet.yourdomain.com/nodo.js"></script>
<script>
window.onload = function () {
  NodoRequestApplication('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', 'https://mainnet.yourdomain.com', function () {
    NODO_START();
  });
};
</script>

NodoRequestApplication(uid, node, callback, options) asks the node for the app over a live socket.io connection, injects its HTML, CSS and JavaScript into this page, then calls callback — normally just NODO_START(). Without a callback it calls NODO_START() itself. options: hash (only run the app if its SHA-256 fingerprint matches — pin an exact version), watch (reload automatically when the node's copy changes), onError.

Isolated instead (an iframe, not this page)

Nodo.load('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', {
  node: 'https://mainnet.yourdomain.com',
  target: '#app-container',
  watch: true          // reload the frame automatically when the node's copy changes
});

Nodo.load runs the app inside an isolated <iframe> — its scripts can't touch your page, and cookies/localStorage are replaced with in-memory versions unless you pass trust: 'same-origin'. Good for loading an app you didn't write yourself into a page you did.

Just the raw files

Nodo.request(uid, node) fetches { uid, name, hash, files } with no rendering at all. And opening https://mainnet.yourdomain.com/<uid> directly in any browser is a ready-made page that loads and runs that one app by itself — handy for sharing a link, or for the href Studio itself shows you right after a successful send.

Monitoring your network

A Design map of your system can show, live, which parts are up. Give each Nodofi component (Relay, Node, NVM Service, NodoCloudDB, AI Worker, Studio, Website, Node Manager…) its Address / ID, then press Monitor in the toolbar.

ComponentHow it is checked
RelayIts status page (connection count); falls back to a socket connection
NodeA node ID: the registry's online flag. A URL or domain: reachable over HTTP
NVM Service, NodoCloudDB, AI WorkerConnected to the relay under that name (write name@https://relay.example.com for another relay). Nobody is messaged and nothing runs.
RegistryIts API answers
Studio, Website, App Browser, Node ManagerReachable over HTTP

Each shape gets a status light — green up, amber slow (over 2 s), red down (with a red outline), grey unknown — and the Network status panel lists everything down-first with response times, how long each has been in its state, and an event log of every change. Changes pop up, and the browser tab shows (N down). Checks run from your browser every 10 seconds to 5 minutes; the interval, relay and registry are saved with the map. Reporting services, CloudDB and AI workers needs a relay with the presence check (current relay version).

Flow projects

A Flow is a program made of blocks: drag blocks from the palette, wire outputs to inputs, press Run and every wire shows the value it carried. Blocks cover values, math and logic, text, lists, HTTP requests, security, MooChedda payments, Service input/output, calls to other flows and relay workers, and your own code. Publish turns a Flow into a live service at its permanent UID that apps, pages (data-flow), other Flows and the Agentic AI can call.

Every block, with its pins, settings and an example: Flow block reference. An overview: Nodofi Flow.

Agentic AI

The Agentic AI pane (robot icon in the left bar) carries out a goal written in plain words by calling tools. It has three modes:

ModeWhat it does
Run Flow tools in StudioA planner model picks among your Flow projects (and Robot projects as robot skills), fills in their inputs, runs them in Studio's sandbox and answers, with a step-by-step trace.
Mission on a robotSends a mission and chosen skills to a paired robot; the AI then runs on the robot within limits — see Missions on the robot.
Agent Starter via relaySends the goal to your own Agent Starter worker and its tool workers.

Setting up the planner

  1. On a machine with Ollama, ollama pull gemma4.
  2. Run an AI Worker (download) joined to your relay as JozieAILLM2 (WORKER_NAME=JozieAILLM2, RELAY_URL=…).
  3. In the pane set the Relay URL, Studio planner agent (JozieAILLM2) and Planner model (gemma4 by default). Planning uses JSON mode with thinking off.

Good tools

  • Give each Flow a clear name and description, and set the field of each Service input (text, question): the planner sees these. A Service output field answer becomes the reply.
  • "use the Quick QR flow to answer: https://beezifi.net" runs a Flow directly, with no planner needed.
  • Answers that are HTML or images (e.g. a Flow UI component) are shown under the answer, with scripts disabled.
  • For robots, tick Robots the agent may move and keep Ask me before a real robot moves on; skills can take values through Agent input blocks — see Agentic AI drives the robot.

Runs are capped at 1–12 steps, Stop cancels at once, and small-model mistakes (broken JSON, a Flow's name used as the tool, wrong input names) are repaired rather than failing.

Robot projects

A Robot project is a Flow that keeps running and drives a robot: trigger blocks (When started, Every, When sensor…), robot and camera blocks, a simulator beside the canvas, and a Hardware tab with real boards, parts and automatic pin assignment. Run on picks the simulator or a paired robot; Deploy saves the program on the robot. Setting up a real robot, the Agentic AI, skill inputs and missions: Robot Agent guide. An overview: Nodofi Robotics.

App performance

Studio can track a node's own performance over time: open App Performance (the gauge icon in the left activity bar) → Link a node…, enter the node's address and key. The node then reports its last 48 hours of stats (requests, errors, response times, bytes served) to Studio every 5 minutes; Studio keeps 90 days and charts it per node. Only you see stats for nodes you've linked. Unlink from either side any time — the node's own admin page (stops sending) or Studio (deletes what was saved).