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.

Working — describes ArdaPlayer v1.2.3

Reading Arda.config

app/app.js
// 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 ||. A bool turned off is false, and false || true is true: || silently ignores the operator's choice. The same goes for a number set to 0 and 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.config becomes a new object. A variable that kept the old one keeps old values. Read Arda.config.x where 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

typeThe portal showsThe app reads
texta one-line box (the default type)a string
memoa multi-line boxa string
numbera number box, with unit beside ita number
boola switchtrue / false
selecta choice from options — a written list, or the device's own printers or screensthe chosen option's value, a string
colora colour pickera string such as "#ff6600"
listany number of wordssee the note below
rowsa table the operator adds rows toan array of objects (below)
imageaccepted by the manifest; not drawn as a picker yeta string; do not build on it yet
app/manifest.json settings
{
  "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"] }
  ]
}
i
A list setting is an array A 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.
app/app.js
var tags = Arda.config.tags ?? [];   // an array of strings
  • A number has no min or max in the manifest: the portal only checks that it is a number. Clamp it in your code.
  • default must fit the type (a number for number, one of the options for select, an array of words for list) 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.

app/manifest.json settings
{
  "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" }
      ] }
  ]
}
app/app.js
// 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 with key, caption, type, and optionally required, 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.
  • min 0–100, max 1–100 (default 100); item names one row in the portal ("Add camera").
  • A row missing a required field is dropped before it reaches the device.
  • secret: true masks the field in the portal (with a reveal), and an app's settings page is never sent it. secret exists only on a field of a rows setting. 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: true lets 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.config is the project's values with that device's overrides on top. A device override of a rows setting 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.

app/manifest.json
{ "key": "printer", "caption": "Receipt printer", "type": "select",
  "options": { "device": "printers" }, "writable": { "device": true } }
  • printers: the value is the printer's id as Arda.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 perDevice says — the project-wide value has no list to choose from — and takes no default.
  • The device checks the app's write against its list as it is now: Arda.config.set('printer', id) answers INVALID with "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 options and the source in a new device key on the definition.
  • Do not ship it before every device of the project runs v1.2.1: an older player cannot read options written as an object and ignores the whole manifest (how it is checked).

Writing a setting from the app

From ArdaPlayer v1.1.17

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:

app/manifest.json settings
{ "key": "dark_mode", "caption": "Dark mode", "type": "bool", "default": false,
  "writable": { "portal": true, "api": true, "device": true } }
MemberDefaultMeaning
portaltruefalse: the portal shows the value with a closed padlock and cannot change it.
apitruefalse: the HTTP API can read the value but not write it (403 NOT_WRITABLE).
devicefalsetrue: 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 writable means {"portal": true, "api": true, "device": false}: exactly what every setting meant before. A member you leave out keeps its default.
  • "device": true is refused when the pack is checked on a key beginning arda_, and on a rows setting with a secret field: an app never writes a password.
app/app.js
// 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:

errorWhen
NOT_WRITABLEThe definition does not say "device": true, the setting holds a secret field, or the key begins arda_.
UNKNOWN_KEYThe app's manifest has no setting by that key, or the app has no manifest.
INVALIDThe 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.
FAILEDThe device could not keep the write on its disk.
UNSUPPORTEDThe player has no settings store in this run ("This run has no settings to write."). Treat it as a refusal.
  • Applied at once. On ok the new value is already in Arda.config when 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.
  • null clears 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.config and goes nowhere.
  • A setting whose key is set hides the method: Arda.config.set is then that setting's value. Do not name a setting set.
!
Write after the first configuration has arrived A write made before the device's first settings document has reached it is checked against "nothing", and ArdaForm, which does hold a value, answers 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.

arda.dev.json
{
  "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.