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
sudoto 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.
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:
| Setting | What it's for |
|---|---|
NODE_ENV=production | Turns 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_DOMAIN | The 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_URL | Studio's own public address, e.g.
https://studio.nodofi.com. |
TRUST_PROXY=1 | Set 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_IP | This 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_TARGET | The 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_SERVERS | Optional, 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
}
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 withcertbot 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.envfor database credentials, finds every domain withsite_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-askonly means anything to Caddy'son_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.shby hand. apachectl -Sshows 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:
| Field | What 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 directory | Whether 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, screenshots | Shown 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).
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.
| Component | How it is checked |
|---|---|
| Relay | Its status page (connection count); falls back to a socket connection |
| Node | A node ID: the registry's online flag. A URL or domain: reachable over HTTP |
| NVM Service, NodoCloudDB, AI Worker | Connected to the relay under that name (write name@https://relay.example.com for another relay). Nobody is messaged and nothing runs. |
| Registry | Its API answers |
| Studio, Website, App Browser, Node Manager | Reachable 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:
| Mode | What it does |
|---|---|
| Run Flow tools in Studio | A 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 robot | Sends 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 relay | Sends the goal to your own Agent Starter worker and its tool workers. |
Setting up the planner
- On a machine with Ollama,
ollama pull gemma4. - Run an AI Worker (download) joined to your relay as
JozieAILLM2(WORKER_NAME=JozieAILLM2,RELAY_URL=…). - In the pane set the Relay URL, Studio planner agent (
JozieAILLM2) and Planner model (gemma4by 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 fieldanswerbecomes 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).