Start.

From an empty folder to an app that reads a setting, lists records and writes new ones — on your own computer, with no network and nothing registered. Ten minutes. You need ardaplayer (the portal's Downloads page, as in quickstart step 1) and a text editor.

Working — describes ArdaPlayer v1.2.3

1 · The folder

An app is a folder with app/ inside it and index.html at the root of app/. Everything else on this page is optional. Make this layout:

layout
counter/
  app/
    index.html        the app
    app.js
    manifest.json     its settings and datasets
  seed/
    items.json        fake data for your desk (never packed)
  arda.dev.json       how the player runs on your desk (never packed)

2 · Declare a setting and two datasets

app/manifest.json
{
  "datasets": [
    { "key": "items", "access": "ro", "api": "rw", "needForBoot": true },
    { "key": "taps" }
  ],
  "settings": [
    { "key": "title", "caption": "Screen title", "type": "text", "default": "Tap an item", "perDevice": true },
    { "key": "show_counts", "caption": "Show counts", "type": "bool", "default": true }
  ]
}

items is filled from outside (the portal or the HTTP API) and is read-only to devices; the app waits for it before it starts. taps is what devices write. title may differ per device. The manifest page has every key.

3 · Seed data and desk settings

In a local run the player fills an empty dataset from seed/<dataset>.json, so you have something to draw before any server holds data. Each record carries its key:

seed/items.json
[
  { "key": "tea",    "name": "Tea",    "colour": "#2f6f4f" },
  { "key": "coffee", "name": "Coffee", "colour": "#6f4a2f" },
  { "key": "cake",   "name": "Cake",   "colour": "#8a3b5c" }
]

arda.dev.json sits beside app/, never inside it. Its settings stand in for the values an operator would type in the portal:

arda.dev.json
{
  "window": "1024x600",
  "console": true,
  "watch": true,
  "settings": { "title": "Front desk" }
}

4 · The page

app/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Counter</title>
  <style>
    body { margin: 0; font: 20px system-ui, sans-serif; background: #111; color: #eee; }
    h1 { margin: 24px; font-size: 28px; }
    #items { display: flex; flex-wrap: wrap; gap: 16px; margin: 24px; }
    button { min-width: 160px; min-height: 96px; border: 0; border-radius: 12px; color: #fff; font: inherit; }
  </style>
</head>
<body>
  <h1 id="title"></h1>
  <div id="items"></div>
  <script src="app.js"></script>
</body>
</html>
app/app.js
// Arda exists before this line runs. Arda.config is a plain frozen object.
function title() { return Arda.config.title ?? 'Tap an item'; }
function showCounts() { return Arda.config.show_counts ?? true; }

// A record synced from ArdaForm is an envelope {key, data, deviceId, seq, createdAt, updatedAt};
// a seed record (and a record you pushed, read back on a desk) is stored as written.
// fields() gives the record's own fields either way.
function fields(r) { return (r && r.seq !== undefined && r.data !== undefined) ? r.data : r; }

var counts = {};

function draw() {
  document.getElementById('title').textContent = title();
  // dataset() can resolve null before anything has arrived: treat that as empty
  Arda.db.dataset('items').then(function (rows) {
    var box = document.getElementById('items');
    box.textContent = '';
    Object.keys(rows || {}).forEach(function (key) {
      var item = fields(rows[key]);
      var b = document.createElement('button');
      b.style.background = item.colour || '#333';
      b.textContent = item.name + (showCounts() ? ' · ' + (counts[key] || 0) : '');
      b.onclick = function () { tap(key); };
      box.appendChild(b);
    });
  });
}

function tap(key) {
  counts[key] = (counts[key] || 0) + 1;
  draw();
  // The one way up: a durable outbox. Give every record a key of your own.
  var id = key + '-' + Date.now();
  Arda.db.push('taps', { item: key, at: new Date().toISOString() }, id)
    .then(function (r) { console.log('queued tap', id, 'as seq', r.seq); },
          function (e) { console.error('could not record the tap:', e.message); });
}

draw();
Arda.on('sync', function (name) { if (name === 'items') draw(); });  // more data arrived
Arda.on('config', draw);                                               // a setting changed

5 · Run it

shell inside counter/
$ ardaplayer -dev .
level=INFO msg="seeded a local dataset" dataset=items records=3
level=INFO msg="local mode: no control plane will be dialled"

A window opens with three buttons and the title Front desk. Tap one: the count moves and the console says the tap was queued. Change "title" in arda.dev.json and restart, or edit app.js and the page reloads (that is "watch": true). Nothing has left your computer: the phase is local, and queued records wait in the outbox until the folder becomes a real device.

6 · Where next

  • Make it a real device — "device": true in arda.dev.json; it shows an activation code you enter in the portal. Before that, read the order of operations: the datasets have to exist on ArdaForm before records can land in them.
  • Put it on hardware — push the folder to GitHub, connect it in the portal and install a device image: quickstart steps 6–8 and the deployment guide.
  • Understand the data — Datasets and sync explains what a device sees and when.

Checked: the manifest above passes ArdaForm's own pack-time check (the plane's lib/manifest.js parser) and the player's parser; the seed format, local echo of a keyed push, dataset() answering null for an empty store, and the config values were observed running ardaplayer (built from source: v1.1.16 and ten unreleased commits) on these files. The window itself (DOM drawing) was not driven by a test here.