NodeApp API

Write small apps that Jaw nodes host: polls, games, forms, tools for a community. A NodeApp is a JavaScript script that runs only on the node, in a sandbox. It never runs on anyone's device: it sends screens built from a fixed set of elements, and the Jaw app draws them with its own controls.

How NodeApps work

Get jaw-app

jaw-app is the NodeApp SDK: it makes an app from a template, runs it on your machine in the same sandbox nodes use, with a preview drawn by the Jaw app's own screen component, checks it, and signs it. It comes in the Jaw API package, next to the bot SDK.

Jaw API 0.0.23

33.8 MB · SHA-256 d63e721265da9a9bf6442a407c125d982111a80492d30fb9cfd42e8fb29e2fbe

Download the Jaw API

It comes from this node, like everything on the downloads page: make sure you trust the node, or compare the SHA-256 with another node's.

You need the .NET 10 SDK (dot.net). Unzip the package, then from its folder:

dotnet tool install --global Jaw.AppKit --add-source ./packages --version 0.0.23

From the Jaw repository instead: dotnet run --project tools/Jaw.AppKit -- <command>.

CommandWhat it does
jaw-app new <template> [folder]Starts an app: counter (one count per person), poll (one vote each in a room), tictactoe (two players take turns), lightsout (a single-player puzzle with a shared best score), weather (fetches a forecast from the web: see Reaching the web).
jaw-app run [folder] [--port N]Runs it at http://127.0.0.1:5199. Pick who you are (Alice, Bob or Carol) and the mode; saving a file reloads it. Nothing leaves your machine.
jaw-app check [folder]Reads the folder as a node would and opens every mode's first screen.
jaw-app pack [folder] [--key F]Signs it into Name-version.JawApp with your author key.
jaw-app key [--key F]Shows (or makes) your author key: ~/.jaw-app/author.key. Back it up; sign later versions with it.
jaw-app editor [folder]Adds editor help (new apps get it): types for the script API and the manifest's schema, so editors such as VS Code complete and check your files. Never packed.

Quick start

jaw-app new counter my-app
jaw-app run my-app

That makes a folder with two files. manifest.json:

{ "name": "Counter", "description": "Counts your presses, and everyone's", "version": "1.0.0",
  "main": "main.js", "modes": ["individual"], "storageBytes": 1024 }

main.js:

function onOpen(session) { return view(session); }

function onAction(session, action) {
  if (action.button === "add") {
    session.state.count = (session.state.count || 0) + action.values.step;
    storage.set("total", String(Number(storage.get("total") || 0) + action.values.step));
  }
  return view(session);
}

function view(session) {
  return { title: "Counter", body: [
    { type: "text", text: "You: " + (session.state.count || 0) + " · everyone: " + (storage.get("total") || 0) },
    { type: "group", title: "Add", children: [
      { type: "number", id: "step", value: 1, min: 1, max: 10 },
      { type: "button", id: "add", text: "Add", style: "primary" } ] } ] };
}

Change the text, save, and the preview reloads. When it works: jaw-app check my-app, then jaw-app pack my-app, and give the .JawApp to a node's owner (Publishing).

The manifest

FieldMeaningDefault, limits
nameShown to peoplerequired, 1-48 characters
descriptionOne lineup to 300 characters
versionPut on the package by jaw-app pack (nodes ignore it)1.0.0
mainThe scriptmain.js, at most 512 KB
modesindividual, room, room-pseudonyms (Sessions)["individual"]
storageBytesRoom for the app's own store0, up to 16 MB
runMillisecondsTime per action200, up to 2,000
memoryMegabytesMemory allocated per action (everything allocated during the run counts)16, up to 128
networkHosts the app reaches (https), or ["any"] alonenone; up to 16 hosts

The node's owner sees what the manifest asks for and approves it when installing. People using the app never see it (an app could lie in it); they see the name, description, AppID and who signed it.

The script

Define two functions; both return a screen:

session is { id, mode, state, member }:

FieldWhat it is
idA stable name for this session. The app never learns who anyone is.
modeindividual, room or room-pseudonyms.
stateAn object you may change; kept for the session's next action (at most 16 KB as JSON).
memberIn room-pseudonyms mode, a stable name for the person pressing (different in every session and app); otherwise null.

Each action gets a fresh sandbox: keep nothing in global variables. A script that runs too long, uses too much memory, recurses too deep, throws, or returns something that isn't a valid screen fails that action only: the person sees why, and nothing it changed is kept.

Reaching the web

An app can fetch data, a weather forecast for example, from the hosts its manifest lists under network, or from any website with ["any"]. The node makes the requests, never anyone's device, so the website sees the node, not the person. Make onOpen/onAction async and await http:

async function onOpen(session) {
  const data = await http.getJson("https://api.open-meteo.com/v1/forecast?latitude=59.9&longitude=10.7&current=temperature_2m");
  return { title: "Oslo", body: [ { type: "text", text: data.current.temperature_2m + " °C" } ] };
}
CallWhat it does
http.get(url, headers?)Resolves to { status, body, contentType }.
http.post(url, body, headers?)The same, with a text body (set content-type in the headers).
http.getJson(url, headers?)GETs and parses JSON; rejects unless the status is 2xx.

Fixed rules, whatever the manifest says:

The owner sees what the app reaches when installing it (installing is the consent), and can turn one app's network off later in Settings → Apps on this node. A refused or failed request rejects with the reason; catch it to show something useful.

Sessions and modes

ModeWho shares statememberGood for
individualnobody: one session per personnullprivate tools, forms
roomeveryone who can read the app channel (or the message that embeds the app)null: you can't tell people apartshared boards, counters
room-pseudonymsthe same: one shared sessiona different stable name per personvotes, turns, games

Room sessions follow the room. When someone is removed, the room's session moves to a new key they don't have, and your app keeps its state; you don't see this happen. An app channel and each message that embeds your app are separate sessions. In jaw-app run, Alice, Bob and Carol are three people in the same room.

Screens

Return { title, body }, where body is a list of elements shown top to bottom:

{ title: "Weather",
  body: [
    { type: "heading", text: "London" },
    { type: "text", text: [ "Now ", { text: "14°C", style: "bold" }, " and dry." ] },
    { type: "group", title: "Another city", children: [
      { type: "textbox", id: "city", label: "City", maxLength: 40 },
      { type: "button", id: "show", text: "Show", style: "primary" } ] } ] }

Elements

typeFieldsNotes
column, rowchildrenStack elements down, or side by side (wrapping).
grouptitle, childrenA form: a button inside sends only the inputs inside.
dividerA line.
headingtext, level (1-3)
texttext: a string, or a list of strings and { text, style }Styles: plain, bold, italic, code. Line breaks are kept.
listitems (strings)Up to 200 items.
tablerows (lists of strings), headerUp to 1,000 cells.
progresspercent (0-100), label
imagedata (base64 PNG or JPEG), altAt most 4 per screen, 64 KB and 1,024 px a side. Never a URL: fetching one would reveal the person's IP address.
linktext, url (http or https)Jaw shows the address and warns before opening it outside Jaw.
textboxid, label, value, placeholder, multiline, password, maxLengthpassword hides what's typed.
numberid, label, value, min, maxWhole numbers.
checkbox, toggleid, label, checked
choiceid, label, options (strings, or { value, label }), selected, dropdownRadio buttons, or a dropdown. Up to 100 options.
buttonid, text, style (normal, primary, danger)

Ids (inputs and buttons): 1-64 letters, digits, -, _ or ., unique within the screen. There is no styling beyond this: Jaw decides how everything looks.

Actions

When someone presses a button, the action holds its id and, for every input in the same group (or, for a button outside any group, every input outside a group): text boxes and choices as text, checkboxes and toggles as true or false, numbers as whole numbers. Inputs nobody touched send the value the screen gave them.

The node checks every action against the screen it answers before your script sees it, so an action can't name a button or input the screen didn't have, or a value it doesn't allow (too long, out of range, not an option).

Storage

storage.get(key) and storage.set(key, value) (strings; null removes) reach the app's own store on the node, shared by every session, within storageBytes. set returns false when the store is full. Use session.state for what belongs to one session.

Limits

WhatLimit
A screen, encoded256 KB, 500 elements, 8 levels deep
Texts and titles4,000 and 100 characters
Session state16 KB as JSON
Work per action5,000,000 statements, recursion 128 deep, plus runMilliseconds and memoryMegabytes
Requests60 a minute per session
Sessions kept1,000 per app (the least recently used go first)

Publishing: install, list, AppIDs

  1. jaw-app pack my-app writes my-app/Name-version.JawApp, signed by your author key. Owners see it as "by account …" with your key's fingerprint; sign every later version with the same key.
  2. A node's owner installs it in the Jaw app: Settings → Apps on this node. The node's own people can open it from there.
  3. To offer it to everyone, the owner presses List in Apps and gives it an AppID, a network-wide name like nodeapp://counter. AppIDs are claimed like handles: a little proof-of-work, backed by witness nodes, fixed after 6 hours, renewed monthly by the node. App names never use up handles.
  4. The app then appears in everyone's Apps tab. Its advert names no node: it's published through a forwarder, and requests reach the app through three forwarders.

The same app on five nodes is five apps, with five keys and five AppIDs.

In rooms: app channels and embeds

What apps learn (and don't)

People are warned that an app and its node can see and keep everything they send it. In return, your app never learns who they are: in individual mode it sees a separate stranger per person (and per app), in rooms only the room's session and, if you asked for pseudonyms, a name per person that means nothing outside your session. It doesn't see the room's members or messages. Ask for no more than you need.

NodeApps or bots?

NodeAppBot
Runson a node, in a sandboxin your own program, anywhere
LanguageJavaScriptC# (.NET)
Talks throughscreens Jaw drawsmessages, slash commands, buttons
Knows who you arenoyes: it's a member of the room
Network, fileshttps to the hosts it lists, through the node; no fileswhatever your program can do

Troubleshooting

You seeWhat to do
"The app's screen has changed; open it again."The press didn't fit the screen that session was last shown (someone else changed a shared session). Reopen it.
"This app doesn't offer that kind of session."Add the mode to modes in the manifest.
"The app took too long" / "used too much memory"Raise runMilliseconds or memoryMegabytes (within the limits), or do less per action.
"The app's screen isn't valid: …"The message names the rule. jaw-app run shows the same message.
A new version shows a different authorIt was signed with another key: use the backed-up ~/.jaw-app/author.key.