Settings.
Settings are the values somebody changes without touching your code: a shop's name, a currency, the cameras on a wall. You declare them in the manifest; operators set them in the portal for the whole project or per device; the app reads them as Arda.config.
Reading Arda.config
// A plain frozen object, ready on your first line. No promise, no wait.
var currency = Arda.config.currency ?? 'GBP';
var showVat = Arda.config.show_vat ?? true; // ?? keeps a deliberate false
// It is REPLACED, never edited, when an operator saves: read it again each time.
function tillName() { return Arda.config.till_name ?? 'Till'; }
Arda.on('config', function (c) {
// c is the new Arda.config, already swapped in
document.title = tillName();
});
- A setting nobody has set is absent, never
null. That is why??is the right way to read one: your default lives in your code. - Use
??, not||. Aboolturned off isfalse, andfalse || trueistrue:||silently ignores the operator's choice. The same goes for a number set to0and an empty text. - Frozen. Assigning to it changes nothing anywhere. Up to v1.1.16 an app cannot write its settings at all; from v1.1.17 it can write this device's own value of a setting whose manifest allows it, with
Arda.config.set(below). - Replaced wholesale. When values change,
Arda.configbecomes a new object. A variable that kept the old one keeps old values. ReadArda.config.xwhere you use it. 'config'fires only when the values for this device actually changed, about three seconds after an operator saves.- Keys beginning
arda_are the player's; the manifest refuses them.
Every type
type | The portal shows | The app reads |
|---|---|---|
text | a one-line box (the default type) | a string |
memo | a multi-line box | a string |
number | a number box, with unit beside it | a number |
bool | a switch | true / false |
select | a choice from options — a written list, or the device's own printers or screens | the chosen option's value, a string |
color | a colour picker | a string such as "#ff6600" |
list | any number of words | see the note below |
rows | a table the operator adds rows to | an array of objects (below) |
image | accepted by the manifest; not drawn as a picker yet | a string; do not build on it yet |
{
"settings": [
{ "key": "shop_name", "caption": "Shop name", "type": "text", "perDevice": true },
{ "key": "receipt_footer", "caption": "Receipt footer", "type": "memo", "help": "Printed under every receipt" },
{ "key": "idle_after", "caption": "Back to start after", "type": "number", "unit": "s", "default": 60 },
{ "key": "show_vat", "caption": "Show VAT", "type": "bool", "default": true },
{ "key": "currency", "caption": "Currency", "type": "select", "default": "gbp",
"options": [ { "value": "gbp", "caption": "Pounds" }, { "value": "eur", "caption": "Euros" } ] },
{ "key": "accent", "caption": "Accent colour", "type": "color", "default": "#f37021" },
{ "key": "tags", "caption": "Tags", "type": "list", "default": ["front"] }
]
}
list reaches the app as an array of strings, in a local run and on a device alike. (Before 19 October 2026 ArdaForm sent it to devices as JSON text of that array; that was fixed on ArdaForm's side, so every player version now gets the array.) A list nobody has set is absent, like any other setting. If you need structure, use a rows setting.
var tags = Arda.config.tags ?? []; // an array of strings
- A
numberhas nominormaxin the manifest: the portal only checks that it is a number. Clamp it in your code. defaultmust fit the type (a number fornumber, one of the options forselect, an array of words forlist) or the pack fails. It seeds the project's value once, when the setting is created.
rows: a list the operator edits
A rows setting is a short list of small objects — cameras, buttons, opening hours. The operator adds, removes and reorders rows in the portal.
{
"settings": [
{ "key": "cameras", "caption": "Cameras", "type": "rows", "perDevice": true,
"min": 0, "max": 6, "item": "Camera",
"fields": [
{ "key": "name", "caption": "Name", "type": "text", "required": true },
{ "key": "url", "caption": "Stream address", "type": "text", "required": true },
{ "key": "user", "caption": "User", "type": "text" },
{ "key": "password", "caption": "Password", "type": "text", "secret": true },
{ "key": "fit", "caption": "Fit", "type": "select", "options": ["contain", "cover"], "default": "contain" }
] }
]
}
// An array of objects, in the order the operator arranged them.
var cams = Arda.config.cameras ?? [];
cams.forEach(function (c, i) {
// a field nobody filled in is absent from its row
console.log(i, c.name, c.fit ?? 'contain', c.user ? 'with a login' : 'no login');
});
fields: 1 to 20, each withkey,caption,type, and optionallyrequired,secret,default,help,options(select),unit(number).- Field types are the single-value ones:
text memo number bool select color image. No list or rows inside rows. min0–100,max1–100 (default 100);itemnames one row in the portal ("Add camera").- A row missing a
requiredfield is dropped before it reaches the device. secret: truemasks the field in the portal (with a reveal), and an app's settings page is never sent it.secretexists only on a field of arowssetting. A password that must be masked therefore lives in a rows setting, even a one-row one ("max": 1).- The device still receives the secret's value in
Arda.config: it is the device that needs it. Never print it, log it or put it on the screen.
Per device, and presets
perDevice: truelets a device override the project's value, on the device's Setup tab in the portal. Without it the project's value is the only value.- The device's
Arda.configis the project's values with that device's overrides on top. A device override of arowssetting replaces the whole list, never row by row. - Presets: on a device's Setup tab, an operator can save that device's own settings under a name and apply them to another device of the project. A preset keeps what the device chose; a setting that followed the project still follows the project's value when applied.
A choice drawn from the device: its printers, its screens
From v1.2.1. A select may take its choices from the device instead of a list written in the manifest: "options": {"device": "printers"} or {"device": "screens"}. In the portal, the device's Setup tab then shows a dropdown of what that device has reported — empty until it has a printer, and following what is plugged in — and the app, when the setting is also writable.device, picks from Arda.printers.list() and writes the id with Arda.config.set. Nobody types a printer id into a box any more.
{ "key": "printer", "caption": "Receipt printer", "type": "select",
"options": { "device": "printers" }, "writable": { "device": true } }
printers: the value is the printer'sidasArda.printers.list()gives it (stable across a replug when the printer has a serial number); the caption its make and model.screens: the connected monitors, the value the panel's key (the same the portal's Primary screen uses), the caption its make and model, with the socket (DP-1) in brackets.- Such a setting is per device whatever
perDevicesays — the project-wide value has no list to choose from — and takes nodefault. - The device checks the app's write against its list as it is now:
Arda.config.set('printer', id)answersINVALIDwith "Must be one of the device's printers: …" (or "…, and it has none right now.") for an id it does not have. - A device whose printer is unplugged keeps its value; the portal shows it as not attached until it is back.
- Your own settings page is given the resolved list in
optionsand the source in a newdevicekey on the definition. - Do not ship it before every device of the project runs v1.2.1: an older player cannot read
optionswritten as an object and ignores the whole manifest (how it is checked).
Writing a setting from the app
Some settings belong on the device: a dark-mode switch on the till itself, the last printer the operator picked. From v1.1.17 the app can write this device's own value of a setting, when the manifest says a device may.
Each setting definition may carry writable, which says who may change its value:
{ "key": "dark_mode", "caption": "Dark mode", "type": "bool", "default": false,
"writable": { "portal": true, "api": true, "device": true } }
| Member | Default | Meaning |
|---|---|---|
portal | true | false: the portal shows the value with a closed padlock and cannot change it. |
api | true | false: the HTTP API can read the value but not write it (403 NOT_WRITABLE). |
device | false | true: the app on a device may write that device's own value. Never the project's value, never another device's. It makes the setting perDevice as well. |
- A definition without
writablemeans{"portal": true, "api": true, "device": false}: exactly what every setting meant before. A member you leave out keeps its default. "device": trueis refused when the pack is checked on a key beginningarda_, and on arowssetting with asecretfield: an app never writes a password.
// v1.1.16 and earlier have no set: feature-check before you offer the switch.
var canWrite = typeof Arda.config.set === 'function';
async function setDarkMode(on) {
var r = await Arda.config.set('dark_mode', on); // null clears this device's value
if (!r.ok) { showMessage(r.message); return; } // r.error is one word
// Already applied: Arda.config.dark_mode is the new value, and 'config' fired.
}
// ArdaForm can still refuse it later (see below): the value is put back.
Arda.on('configRefused', function (r) {
// r = {key, error, message}; Arda.config already holds ArdaForm's value again
console.log(r.key, r.error, r.message);
});
await Arda.config.set(key, value) resolves {ok: true} or {ok: false, error, message}. It does not reject for a refusal. message is a sentence you can show; error is one of:
error | When |
|---|---|
NOT_WRITABLE | The definition does not say "device": true, the setting holds a secret field, or the key begins arda_. |
UNKNOWN_KEY | The app's manifest has no setting by that key, or the app has no manifest. |
INVALID | The value does not fit the type (a string for a bool, a word not in a select's options, a rows list outside its min/max, over 64 KB), or no value was given: set(key) on its own is INVALID. Use null to clear. |
FAILED | The device could not keep the write on its disk. |
UNSUPPORTED | The player has no settings store in this run ("This run has no settings to write."). Treat it as a refusal. |
- Applied at once. On
okthe new value is already inArda.configwhen the promise resolves, and'config'fires. - Kept while offline. The write is stored on the device, survives a restart, and is sent when the device is connected. Several writes of one key while offline send only the latest.
- ArdaForm takes it only if nothing changed meanwhile. It is applied only if this device's value is still what the device held when the app wrote. If an operator changed it in the portal in the meantime, the app's write is refused with
STALE: the device gets ArdaForm's value back,'config'fires again, and'configRefused'tells the app{key, error, message}. The other words (NOT_WRITABLE,UNKNOWN_KEY,INVALID) can arrive this way too, when ArdaForm's copy of the manifest disagrees with the device's. nullclears this device's own value; the device follows the project's value again.- In the portal, the device's Setup tab shows the value with set on the device at … under it.
- In a local developer-mode run the write changes this session's
Arda.configand goes nowhere. - A setting whose key is
sethides the method:Arda.config.setis then that setting's value. Do not name a settingset.
STALE. Write once Arda.config has values (after the first 'config' on a freshly set-up device), not on the app's first line.
On your desk
In a local developer-mode run, Arda.config is the manifest's defaults with the "settings" block of arda.dev.json on top. Change values there and restart, or use your settings page's Apply in the harness, which updates Arda.config and fires 'config' without a restart.
{
"settings": {
"shop_name": "Test shop",
"show_vat": false,
"cameras": [ { "name": "Door", "url": "rtsp://192.168.1.50:554/stream2", "user": "viewer" } ]
}
}
Checked: both manifests pass ArdaForm's pack-time parser and the player's parser. ?? keeping a false, the frozen object and the values from arda.dev.json were observed in a local ardaplayer run. How each type is delivered is read from the control plane's lib/config.js; list arriving as an array is ArdaForm's change of 2026-10-19 in lib/config.js. Arda.config.set: read from the v1.1.17 player source (internal/client/configset.go, the injected API) and the plane's deviceWrite; not yet run on a released player.