From a folder
to a device.

The whole path, in the order you will walk it: write the app, run it on your desk, connect the repository, make a device, adopt it, watch it, update it, retire it. Every portal screen is shown beside the step, with example data. It is written so that a person — or an AI assistant working from the Markdown copy — can follow it end to end.

Everything here runs on devices today Anyone can sign up — the free plan needs no card

Before you start

●
Basic HTML and JavaScript is enough If you can write basic HTML and JavaScript, you can build an app and run it on one device — or hundreds. Every step below is a file you write, a button in the portal, or a card you flash; the device does the rest. Notes marked Under the hood explain what happens behind the scenes — read them if you are curious, skip them if not.

The example throughout is a gym's member check-in kiosk: tenant Northgate Fitness, project Check-in, a Raspberry Pi in a frame on the reception desk. Swap the names for yours; nothing else changes.

TermMeaning here
TenantYour organisation in the portal. It owns projects, people and API keys.
ProjectOne application and everything around it: branches, datasets, files, settings, devices, install codes, webhooks.
BranchA deployable line of the project — main, staging. Each is connected to one git ref and carries an apply policy. A device follows exactly one branch.
DeviceOne installation of ArdaPlayer with an identity it generated itself, a branch, and a data partition.
PartitionWhich share of the data a device gets — one site's members, one till's prices, one room's bookings.
Datasets · files · settingsWhat ArdaForm keeps up to date on each device: records (a price list, a member list), pictures and documents, and the values an operator types.

You need a workstation (Linux, macOS or Windows), a GitHub account, and a portal account (create one — the free plan needs no card). For steps 1 and 2 you need nothing else. For step 4 you need a Raspberry Pi 3 or 4 with a card, or an x86-64 machine with a USB stick.

01

Write the application

An ArdaForm app is a folder. There is no build step on the platform's side and nothing to install in the page.

The layout is fixed and small. app/ is the whole application — index.html at its root and every asset it uses — and it is the only thing that is ever sent to a device. Everything beside it stays yours, apart from an optional gui/ that the portal reads (below).

checkin/ the application folder
checkin/
├── app/
│   ├── index.html          ← the app's root; the player serves app/ and only app/
│   ├── app.js
│   ├── style.css
│   └── manifest.json       ← optional: datasets, settings, files the app needs at boot
├── arda.dev.json           ← developer settings, BESIDE app/, never packed
├── gui/                    ← optional: the app's own settings page, for the portal
├── seed/
│   └── members.json        ← optional: local records for developing without a server
├── data/  storage/  state/ ← the player's own; created on first run, keep out of git
└── .gitignore

The page is ordinary HTML. The one ArdaForm-specific thing is window.Arda, which the player puts in the page before your first line runs — no script tag, no import.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Northgate · check-in</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header><b id="kiosk"></b> <span id="state"></span></header>
  <main>
    <h1>Scan your member code</h1>
    <button id="type">Type a number instead</button>
  </main>
  <script src="app.js"></script>
</body>
</html>

app/manifest.json — what the app needs

The manifest is the developer's declaration of the datasets the app reads, the settings an operator may type, and the files it wants before it starts. The portal creates and updates these definitions when a release is packed; it never deletes one. Values stay the operator's.

app/manifest.json
{
  "datasets": [
    { "key": "members", "access": "ro", "api": "rw", "partition": "site", "needForBoot": true },
    { "key": "passes",  "access": "ro", "needForBoot": true },
    { "key": "visits",  "access": "rw", "retention": { "days": 400 }, "size": "50MB" }
  ],
  "settings": [
    { "key": "currency",   "caption": "Currency",        "type": "select", "options": ["GBP", "EUR"], "default": "GBP" },
    { "key": "pass_price", "caption": "Day pass price",  "type": "number", "unit": "£", "default": 8 },
    { "key": "kiosk_name", "caption": "Kiosk name",      "type": "text",   "perDevice": true }
  ],
  "files": { "needForBoot": ["banners"] }
}
Dataset keyMeaningDefault
keyThe name the app asks for: Arda.db.dataset('members'). Letters, digits, ._-; outbox is reserved.required
accessrw or ro — whether devices may push into it.rw
apiThe most the HTTP API may do: rw, ro, none.rw
partitionThe record field a device's partition selects on. No field: every device gets everything.none
retention"never", {"days": N} or {"space": true}.never
sizeA storage ceiling: bytes, or "50MB", "1GB".the account's
needForBootThe app waits for this dataset before it starts; the rest sync behind it. None marked means all are waited for.false

Setting fields take key (lower-case; arda_ is reserved), caption, type (text memo number bool select color list image rows), default (seeds the value once, when the field is created), perDevice, help, options, unit. files.needForBoot is "*", a folder name or a list of folders under /storage/ that must be present before the app starts.

A rows setting is a list of small objects an operator adds to and removes from — a set of cameras, say. It takes min, max (up to 100), item (what one row is called) and fields of the plain types (text memo number bool select color), each of which may be required and secret (masked in the portal). Arda.config.cameras is then an array of objects in the operator's order. A top-level partition block makes the portal offer a device's data partition by name:

app/manifest.json a rows setting and a partition block
{
  "settings": [
    { "key": "cameras", "caption": "Cameras", "type": "rows", "perDevice": true,
      "min": 0, "max": 4, "item": "Camera",
      "fields": [
        { "key": "url", "caption": "Stream URL", "type": "text", "required": true, "secret": true },
        { "key": "fit", "caption": "Fit", "type": "select", "options": ["contain", "cover"] }
      ] }
  ],
  "partition": { "caption": "Site", "required": true,
    "options": [ { "value": "northgate-01", "caption": "Northgate" },
                 { "value": "northgate-02", "caption": "Riverside" } ] }
}

The portal uses the partition block only when the project's Setup has Data partition: use the app's list turned on. It is off by default, and then a device's partition is typed in.

gui/ — a settings page of the app's own

The portal's standard form shows every setting the manifest declares. An app can bring its own page for them instead: gui/settings/index.html and its files, in a gui/ folder beside app/. The portal then shows that page under Setup › App settings, and on a device's Setup tab for the per-device settings, with the standard form one click away. The settings are still the manifest's: the page proposes values, and nothing is saved until the operator presses Update. gui/ is never sent to a device, and a commit that changes only gui/ does not update the devices. Settings pages come with every package but Free.

checkin/ with a settings page
checkin/
├── app/                    ← the app, packed and sent to devices as before
│   └── manifest.json       ← the settings, as before
└── gui/                    ← read by the portal, never sent to a device
    ├── icon.svg            ← optional (or icon.png)
    ├── gui.json            ← optional: {"settings": {"title": "Check-in screen", "height": 520}}
    └── settings/
        ├── index.html      ← the settings page
        └── page.css  app.js …

The page runs in a sandboxed frame with no network, no cookies and no local storage, and talks to the portal only through a small helper, ArdaGUI — its calls are on the API page. Files may be HTML, CSS, JS, JSON, images and fonts, up to 200 files and 5 MB; anything else and the portal shows the standard form and says why, without stopping the app's deploy. In developer mode the player prints settings page: http://127.0.0.1:<port>/__arda/gui/ and hosts the page the same way; its Apply puts the accepted values into the running app's Arda.config, in memory only (from ArdaPlayer v1.1.13).

arda.dev.json — your settings for this folder

Beside app/, not inside it. It is read only in developer mode, never packed, and a device refuses to start if it finds one — so it cannot reach a fleet by accident. An unknown key is refused, not ignored.

arda.dev.json
{
  "window":  "1280x800",
  "scale":   "75%",
  "bg":      "#000",
  "console": true,
  "watch":   true,
  "can":     { "touch": true, "keyboard": false, "mouse": false },
  "screens": [ { "name": "main", "primary": true }, { "name": "queue", "size": "800x480" } ],
  "partition": "northgate-01",
  "settings":  { "kiosk_name": "Desk (dev)" },
  "devices":   { "wifi": { "enabled": true, "networks": [{ "ssid": "Northgate staff", "key": "secret" }] } },
  "webhooks":  [{ "slug": "visits", "url": "http://localhost:8080/hook", "key": "dev" }],
  "device":  false
}

The file is plain JSON — no comments, or the player refuses it. What the keys do:

  • window, scale, bg: the app's screen size, how big the window is drawn, and the colour before the app paints.
  • console opens the inspector at start; watch reloads the page on every save.
  • can is what the app is told this machine has; screens are the displays it pretends to have.
  • partition cuts partitioned datasets to this one slice, locally; settings are your values for the app's settings.
  • devices is a pretend Wi-Fi radio; webhooks are called from this machine, signed as ArdaForm would sign them.
  • device: true enrols this folder as a real device and syncs for real.
!
Bundlers If you use Vite, esbuild or similar, commit the output folder and point the connector at it in step 3. ArdaForm never runs anything from your repository — no npm install, no build script — so the folder it packs must already be the finished page.
02

Run it on your desk

ArdaPlayer is one binary. In developer mode it runs your folder in a window, locally, with no account and no device.

Download the build for your workstation from the portal's Downloads page. The Linux desktop build opens a WebKit window of its own; the Windows and macOS builds open the app in your browser. Put it on your PATH.

The Downloads page: ArdaPlayer builds for devices and desks, and the three OS images
Downloads. The device players are what a device updates itself to; the developer builds are for your desk; the OS images are step 4.
~/checkinworkstation
$ ardaplayer -dev . ardaplayer v1.2.3 — ArdaForm device runtime device local — no control plane is dialled; the app runs on its own stores. datasets remembered: members, passes, visits level=INFO msg="serving app" … dev=true
phase localDEVELOPING
  • Local by default. Nothing dials out. Every dataset in the manifest gets a store; what you push is read back at once; seed/<dataset>.json fills an empty store; Arda.config is built from the manifest's defaults plus the settings in your dev file.
  • The device you develop on is not touched. Arda.system.shutdown() shows a notice instead; brightness and volume writes answer simulated: true; Wi-Fi uses the pretend radio from the dev file.
  • Every app folder is its own device. Set "device": true and the folder enrols with an identity kept under state/, syncs for real, and appears in the portal like any other device — adopt it in step 5. Two folders are two devices.
  • No screen? ardaplayer -headless . runs the same page with no DOM. Arda.headless is true before your first line; classic scripts only.
  • Other screens. A screens list in the dev file opens each extra display as a window, and Arda.screen['queue'] is that window's document.
03

Connect the repository

ArdaForm reads your repository through a GitHub App and packs a commit when a device needs it. It never runs your code.

shell workstation
$ cd checkin
$ printf 'state/\ndata/\nstorage/\n' > .gitignore      # the player's own directories; arda.dev.json may stay, it is never packed
$ git init && git add -A && git commit -m "check-in kiosk"
$ gh repo create northgate-digital/checkin-kiosk --private --source . --push
  1. Install the ArdaForm GitHub App on the account or organisation that owns the repository: github.com/apps/ardaform. Give it the repositories it may read. This is the one prerequisite the Connectors tab cannot do for you.
  2. Every project starts with a branch called main, its default — new devices go on it. Add more under Code → Add branch; the branch dialog's Default branch switch moves the default.
  3. Open Connectors → Connect GitHub. GitHub asks you to authorise ArdaForm once; if you already installed the App from its own page, press Connect anyway — the portal adopts the installation.
  4. Add connection: project branch ← repository, folder, GitHub branch. After the first, add each connection with + — it passes through GitHub and comes back to this dialog. The folder is the directory whose index.html is the app's root — here app. One connection per project branch; two branches may read two repositories.
The Connectors tab: one line per project branch with its repository, folder, git branch and last commit
Connectors. One line per project branch: the repository and folder it reads, the git branch, the last commit read, and "deployed" once every device on that branch runs it. Add another with +.
The connection dialog: project branch, repository, folder and GitHub branch
A connection. Project branch, repository, the folder inside it, the GitHub branch — here staging reading next.

Now the branch has a source. The Code tab shows each branch's latest push, how many devices run the branch, and its apply policy — when a device that received an update may apply it:

When updates applyWhat the device does
As soon as they arrive (default)Stages the update and asks the app at once. If the app says nothing for 5 seconds, it applies.
Only inside a time windowStages it, and asks the app only inside the window (device local time; 22:00–05:00 crosses midnight correctly).
Only when the app asks for themStages it and waits for Arda.update.apply(). A restart does not apply it either.
The Code tab: each branch with its latest push and how many devices run it, and the recent pushes
Code. Each branch with its latest push, its update window if it has one, and how many devices run it; below, the recent pushes.
The branch dialog: name, when updates apply, a time window and the Default branch switch
The branch dialog. Its name, when updates apply — here only between 22:00 and 05:00, device time — and the Default branch switch.
i
Under the hood: what "packed" means Every push moves the recorded head (GitHub's webhook, or Refresh on the connection). ArdaForm packs that commit's folder once — as soon as it is pushed, or when a device first asks — into a sealed .arda container (gzip tar, AES-256-GCM; symlinks refused, dotfiles and .env* never included, 4096 files / 64 MiB), reconciles the manifest's datasets and settings, and sends the device a signed instruction naming the sha256 and the key. Package format has the details.
04

Make a device

Three images, one player. Whatever you pick, the machine boots straight into ArdaPlayer with no desktop, no browser and no login.

Raspberry Pi

ardaplayer-rpi-arm64.img.xz. Write it to a card with Raspberry Pi Imager, balenaEtcher or dd. Pi 3 and 4; HDMI or a DSI touchscreen — the image probes the panel at boot and configures itself. The first boot grows the card.

PC image

ardaplayer-linux-amd64.img.xz. Write it straight to the machine's disk (from another OS, or from a live stick). Boots UEFI or legacy BIOS; the first boot grows the root and mints its own host keys.

USB installer

ardaplayer-linux-amd64-installer.img.xz. Write it to a stick, boot the target from it. It is ArdaPlayer running an install app: choose the disk, name the machine, join Wi-Fi, type an install code — by touch or keyboard. At the end it restarts into the installed system, even if the stick is already out.

●
Or an Android tablet No image at all: on a tablet, a display with Android inside or a TV box, open ardaform.net/get/ardaplayer-android.apk, install it and adopt it by its code (step 5). Android devices covers Android’s warnings and kiosk mode.
shell write a card or a stick (Linux)
$ xz -dc ardaplayer-rpi-arm64.img.xz | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
# /dev/sdX is the card (lsblk shows it). On macOS use Raspberry Pi Imager or balenaEtcher, which take the .img.xz directly.

Wi-Fi before the first boot

A Pi with a cable needs nothing. For Wi-Fi, either give it the network afterwards over Bluetooth (step 5), or put a file on the card's boot partition before the first boot — the player reads it at every start and restores the profile if it is ever lost:

/boot/firmware/arda-wifi.conf on the card's boot partition
ssid=Northgate staff
psk=the-password
# hidden=1 for a hidden network

On the USB installer the Wi-Fi step joins the stick itself to prove the password, then writes the same profile to the installed disk.

Your own SSH key

A published image carries no credentials. To be able to log in yourself, put your public key — one per line, several if you like — in arda-ssh.pub on the same boot partition (the Pi's card, the PC image, or the USB stick, which copies it to the disk it installs). The device adds it for its login user (pi on the Raspberry Pi image, arda on the PC) at every start. SSH from the network still stays closed until you open it from the portal (step 6); keys can also be installed from there.

What the first boot looks like

The ArdaForm logo is drawn about 1.3 seconds after power, from the initramfs. (The PC image and the stick also carry it as BMP files on the boot partition, for firmware that lets you pick a boot logo.) The player starts, finds the display, and shows its boot screen until the app is running. Three states you will see:

Activate this devicetype this code in the portal · Devices › Add device
K7X4P
or adopt it from the Adopt tab on the same network

1 · Unregistered: an activation code. Also findable over Bluetooth as arda-mx8c.

Downloading datasetsmembers · visits · passes
about a minute left

2 · Registered: fetching the app and walking the boot datasets and files.

Waiting for approvalregistered with install code "Studio rollout" · locked until an operator approves it

2b · A code marked "needs approval" locks the device: no app until Approve is pressed.

The player's own screens. They belong to ArdaPlayer, not to your app — a device shows them before it has an app, while it syncs, and when it is locked.
i
Already running Debian? An existing Debian-family machine can be made a device without reinstalling: the same script the images run puts the packages, the boot console and the player's service in place. Ask for it through the contact form — the images are the supported path.
05

Adopt the device

A new device has an identity but no owner. Four ways to give it one; pick whichever fits the machine in front of you.

The device…Do this
has a screenIt shows an activation code. Portal → project → Devices → Add device → type the code, give it a name, a branch and a partition (picked by name when the manifest lists them).
is on the same network as your browserDevices → Adopt lists devices dialling in from your network. Press Adopt. Nothing to type: being on the same network is the proof.
has no network and no screenDevices → Adopt → the Bluetooth icon (Chrome or Edge). Pick its arda-xxxx name. The device is handed the Wi-Fi of the project's active profile (Devices → Profiles), joins, reaches ArdaForm, and is adopted with one press.
is installed by somebody else, laterInstall codes → New code. Put the code on the USB installer at install time. The device registers itself on first contact — locked until you press Approve if you said so.
portal.ardaform.net/projects/12/devices
ArdaFormProjects / Check-inNorthgate Fitness · 121/137
Code Connectors Data Devices Install codes Webhooks API keys People Marketplace
DevicesProfilesAdoptBT↻
2 waiting
Raspberry Pi 3 Model B Rev 1.2on your network …7c19e4 player v1.1.16 just now
Adopt
arda-7pm2Bluetooth …a83f02 player v1.1.16 no network yet
Adopt
Adopt. Devices waiting on your network, and one found over Bluetooth that has no network yet.
The Add device dialog with a five-character activation code typed in
The code from its screen. Devices → Add device, type it, Find device.
Add device
DeviceRaspberry Pi 3 Model B Rev 1.2Hardware…7c19e4Runtimelinux/arm64 · player v1.1.16
Studio door
Studio 2, by the door
main
Northgate
Which partition of the shared datasets this device reads.
CancelAdd device
Adding a device. The model and hardware it reported, then a name, location, branch and data partition.
Adopt over Bluetooth BT
arda-7pm2Bluetooth …a83f02 on Northgate staff, connecting…
Adopting…
07:12:04 adopt sending the "Staff Wi-Fi" profile’s network Northgate staff… 07:12:11 adopt joined Northgate staff; waiting for the device to reach ArdaForm… 07:12:16 adopt found — name it and add it
Over Bluetooth. The device is handed the active profile's network, joins it, reaches ArdaForm, and opens Add device.
The Install codes tab: codes with how many devices used them, branch, partition, approval and expiry
Install codes. A name, how long it is valid, how many devices, which branch and partition they land on, and whether an operator must approve each one. A revoked code stays as the record of what it registered.
The Devices tab: each device with its branch, club and state
Devices. Each device with its branch, its partition (here the club) and its state: online, offline for a while, or not seen since it was added.

Within half a minute of adoption the device fetches its branch's package, walks its datasets and files, and starts the app when the boot set is current. Profiles (Devices → Profiles) are Wi-Fi networks kept per project. The active one is handed to a device you adopt over Bluetooth; a running device gets another network on its Peripherals tab.

06

Watch it run

Open the device. Seven tabs, one refresh, and everything comes over the device's own outbound connection — no port on its network is opened for any of it.

One device: status chips, the actions bar above the tabs, and the Device tab
Device. Status across the top, the actions bar above the tabs, then the machine: model, system, memory, disk, screen, uptime and what the app is told the machine has.
The Device tab close up: machine tiles, brightness and volume sliders, the device password and live graphs
The Device tab, further down. Brightness, scale and volume you can move (and pin for every boot), the device password, and live CPU, memory and network graphs.

Above every tab sits the actions bar: Screenshot (a lossless picture of every attached screen), Approve (a device an install code held back), New password, Apply update, Reboot, Shut down and Remove.

TabWhat it does
DeviceHostname, model, OS, local IP, player version, branch and what is deployed or staged, uptime, screens. Brightness, contrast and volume sliders — whatever the attached screen and card allow, HDMI monitors included — and a scale per screen from 50 to 150 %. The device password.
PeripheralsWi-Fi (scan, test a password, save, forget, order), Bluetooth, printers (what was detected, an override for the paper width, four test prints), a serial probe for an unknown adapter, screens identified by their EDID rather than their socket, the system timezone. Network printers: Find printers on the network asks every address on the device's own network once for anything that takes print jobs on port 9100 — an office printer may appear too — lists what answered with its name and model where it gave one, and adds the one you choose; a printer the scan cannot see is added by its address. A network printer has a Remove action, and the width, protocol and test prints work for it as for a USB one. Needs the player release after v1.2.4.
SetupName, branch, data partition, per-device setting overrides — in the app's own settings page, if it has one. Changing the branch or partition resyncs the device: it drops its data, shows "Configuration changed — resyncing", and starts the app on the new data. Detection priority sets how soon the portal reports the device offline when it loses power or its network — about 35, 25 or 15 seconds, as your package allows. On a device with two monitors, Primary screen chooses which one the app is on: System order (the first connected socket) or a particular monitor, which the app then follows into whatever socket it is plugged into. Every other monitor can show a camera (Arda.camera.show); the app can make the choice too. Moving the app restarts it on the new screen. This needs a PC-type (x86) device installed from the v1.1.12 images or later; the Raspberry Pi image does not have it yet, and a device that only receives the newer player keeps one screen.
Alerts & logAlerts the device raised (a printer it cannot write to, low power, a full disk). Log capture on demand: switch it on and the last few hundred lines that already happened arrive first; it switches itself off.
Device viewA picture of the screen, on demand or every 5, 10 or 30 seconds; the app's console as it happens; an expression evaluated in the page; the page source; and "Pretend a screen" for an HDMI device with nothing plugged in.
SSHA root shell, opened by you, tunnelled over the device's connection. The plane asks the device for a target by name; a device offers ssh, view and shell and nothing else. Beside it, External SSH: port 22 from the shop's network, closed on every device behind its firewall until you open it here — for 30 minutes, 5 hours, until you close it, or while this screen is open. The device closes it on time by itself; an open door offers Extend or Close now. The user icon installs a user and a public key, expiring after a day, a month, or never. A user it creates has no password and no sudo — it can watch the app in the device view and read the logs; root is a tick, Passwordless sudo, given only with an expiry, and every key of that user then expires with it. Keys only, no root login.
BluetoothReach this device over Bluetooth when it has dropped off the network: the networks it hears, the one it is on, and a join form whose password is encrypted to the device.
portal.ardaform.net/projects/12/devices/40/view
ArdaFormProjects / Check-in / Devices / ReceptionNorthgate Fitness · 121/137
DevicePeripheralsSetupAlerts & logDevice viewSSHBluetooth↻
Device view Manual5s10s30s▭</>
Northgate Fitness · Reception online · 07:41
Scan your member codeHold your member QR to the scanner, or type your number.
Visits today 212
Spin · 08:00 4 places
Day pass £8.00
v1.4.1
Check inType numberDay passHelp
Connected · 1280 × 800 · 61 KB
Arda.db.status()
07:41:02 > Arda.db.status() 07:41:02 < {ready: true, datasets: {members: {synced: true, boot: true, records: 1842}, visits: {synced: true, records: 12930}}, files: {done: 20, total: 20, complete: true}} 07:41:19 app welcome: member 10231 · visit queued seq 4471 07:41:19 app outbox: 1 delivered
Device view. The screen as the device draws it, refreshed when you ask; the console; an expression answered by the running page.
Alerts and log of one device: alerts with their state, and log lines with capture switched on
Alerts & log. Alerts the device raised — a printer it cannot print to, low power, a screen unplugged — open or cleared; below, the player's lines and the app's console.*, with capture switched on for a while.
portal.ardaform.net/projects/12/devices/40/shell
ArdaFormProjects / Check-in / Devices / ReceptionNorthgate Fitness · 121/137
DevicePeripheralsSetupAlerts & logDevice viewSSHBluetooth↻
SSH External SSH · open · 28 min left>_+
root@arda-9k2f:/# uptime  07:52:10 up 6 days, 4:11, load average: 0.21 root@arda-9k2f:/# journalctl -u ardaplayer -n 2 -o cat INFO webhook called slug=visits status=200 ok=true ms=143 INFO ssh key installed user=sam sudo=false expires=8 Oct root@arda-9k2f:/#  
Add an ssh user
Install your public key on this device. A user that does not exist yet is created without a password: it can watch the app in the device view and read the logs, and is not root unless you tick passwordless sudo.
sam
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK… sam@laptop
1 day
Passwordless sudo (root)
CancelInstall
SSH. A shell through ArdaForm, External SSH opened for a while, and your own key installed with an expiry.

The data behind the app

The project's Data tab holds the datasets the manifest declared, the records in them, and the files the devices sync. Records reach a dataset from devices (Arda.db.push), from the portal, or from the HTTP API with a key minted under API keys — api.ardaform.net/v1, described by /v1/openapi.json. A partitioned dataset sends each device only the records whose partition field matches the device's partition.

The API keys tab: keys with their access, scope and expiry
API keys. Each key with what it may do, how far it reaches and when it expires; a revoked key stays as the record of what it could reach.
The Data tab: datasets with their access, partition and boot pills, and one no longer in the manifest
Datasets. Each with its access, its partition field and whether the app waits for it at boot; promotions is no longer in the manifest and is kept until you delete it.
The Files tab: the project files as a tree, with the folder needed at boot
Files. The file library as a tree; banners is needed at boot, and a file can be limited to some partitions.
The project Setup page: the project, and the app settings with their values
Setup. The project, and the app's settings. The settings are declared in app/manifest.json; their values are set here (project-wide) or on a device's Setup tab. A saved change reaches a running device in about three seconds as Arda.config. An app with a settings page of its own shows that page here instead, with this form one click away.
One device's Setup tab: name, location, branch, partition, and its own values for per-device settings
One device's Setup. Its name, branch and partition, and its own values for the per-device fields. Save as preset and Apply preset copy them to the next device.

Webhooks — calling your own servers

The project's Webhooks tab — or /v1/webhooks in the HTTP API — defines endpoints of yours the app may call: a name, a slug, the URL, the method (POST, GET or PUT), an optional key, Verify, and enabled. The app names one by its slug; ArdaForm makes the request from its own servers, so the key never reaches a device and your firewall has one source to allow. Every delivery is logged on the tab with its status, time and device. Leave the URL empty and nothing is sent: the calls are only recorded (the app gets ok: true, status 202), each opens in Deliveries as its JSON, and your own systems can read them back through /v1/webhooks/{slug}/deliveries.

app/app.js excerpt
// Resolves {id, ok, status, ms, body, error}. ok is a 2xx; a 4xx or 5xx is an answer.
// It rejects only when nobody was asked: offline, unknown or disabled webhook, the limit.
Arda.webhook.call('visits', { member: m.id, kiosk: Arda.config.kiosk_name })
  .then(function (r) { if (!r.ok) console.warn('CRM answered', r.status) })
  .catch(function (e) { console.warn(e.message) })

If you write the server at the other end: it receives JSON — {id, webhook, project, device: {uid, name}, at, verify, data} (a GET webhook gets the same fields as query parameters, device as its uid and data JSON-encoded) — with X-Arda-Delivery, X-Arda-Webhook, X-Arda-Project and X-Arda-Timestamp. With a key it also gets Authorization: Bearer <key> and X-Arda-Signature: sha256= the hex HMAC-SHA256 of timestamp + "." + body; with Verify on, a one-time X-Arda-Verify token it can confirm with POST /v1/webhooks/verify. ArdaForm waits 10 seconds, follows no redirects, refuses private addresses, and hands the app the first 4 KiB of the answer; data may be up to 64 KiB. Whether webhooks are included, how many an account may define and how many calls a minute it may make come with the package. On your desk, the webhooks list in arda.dev.json stands in for them.

The Webhooks tab: webhooks with slug, method, host, key and verify, and the delivery log
Webhooks. Each with its slug, method, host and whether it has a key and Verify; what the last call did; one that only records; and every delivery with its status, time and device.
One recorded delivery: the JSON envelope that would have been sent
A recorded delivery. A webhook with no URL keeps each call as its JSON, ready for your systems to read back through the API.
07

Update it

Push. That is the whole of it from your side; the rest is what the device does with it.

  1. 01
    The head movesGitHub tells ArdaForm; the Connectors row shows the new commit, "ahead of what is deployed".
  2. 02
    Packed once, sent to each deviceA signed instruction per device, with the sha256 and the key. A device that is offline gets it when it returns.
  3. 03
    Fetched, verified, stagedThe device checks the bytes against the signed sha, proves the container opens, writes it beside the running app with a READY marker last. The old app keeps running.
  4. 04
    The app picks the momentupdateStart fires. Say nothing and it applies in 5 seconds; pause() buys 60 seconds, up to five times; apply() does it now. After five pauses it stays staged until the next restart or an operator's Apply update.
  5. 05
    Everything at onceDevices in one shop apply within tens of milliseconds of each other. The portal shows what each runs and what it holds staged.
!
Going back There is no separate rollback button: point the connection at an older ref, or push a revert — it is an update like any other. The player's own previous binary is kept for its rollback guard (below).

The player updates itself

A new ArdaPlayer release is a signed instruction too, naming the build for the device's OS and architecture with its sha256. The device downloads it, runs it once to prove it starts, and arms it for the next restart — the running process is untouched until then. If the new build cannot stay up for ninety seconds three starts in a row, the device puts the previous one back by itself. A release marked important goes through the same five-second negotiation with your app instead of waiting for a restart.

08

Retire it

A device leaves in one of three ways, and each leaves a different trace.

  • Remove on the device screen issues a wipe. The device acknowledges it, flushes what its outbox still holds for up to thirty seconds, erases its identity, its app and its data, and restarts as a fresh device showing a code. The row stays as "decommissioned". A device that is switched off takes the wipe whenever it next connects — the wipe waits for it for a year — and so does a stolen one.
  • ardaplayer -unbind at the device does the same from its own side. It asks for the device password — eight characters the portal shows on the Device tab and can reissue with New password; the device holds only a hash of it. The control plane forgets first and the device erases itself second, so an interruption is safe to repeat. The row fades as a "ghost" over seven days.
  • Forget completely deletes the row — its alerts, log and settings — for a device that is gone: a card you reflashed, hardware you retired. The wipe does not go with the row. ArdaForm keeps it apart for a year, so if that card is ever switched on again it is wiped the moment it connects (within a minute, if it is connected when you press it) and shows a new code. A device with a wipe already waiting offers Delete anyway instead, with the same guarantee.

Deleting a whole project does the same for every device in it: each one stops being able to fetch code or files at once, is wiped the next time it connects, and has to be claimed again in person, from the code on its own screen. A device removed in any of these ways no longer goes on running what it had.

When something looks wrong

You seeIt usually means
"Registered — waiting for this device's application"The device's branch has no connection, or the connection's folder has no index.html. Check Connectors, then the Code tab's head.
"Waiting for approval"It registered with an install code that needs approval. Press Approve on the device screen.
The app runs but shows nothing / empty listsThe device's partition matches no records, or a boot dataset is still walking. Arda.db.status() in Device view says which.
"The device service is not answering"The device is restarting (an update, a reboot, a timezone change). Try again in a moment; the log tab says why it left.
Device view has no picture on a headless PiNothing is plugged into HDMI, so there is no screen to capture. "Pretend a screen" gives it one at 1024×768 until a real one is attached.
A pack fails with a manifest faultThe portal refuses an unknown manifest key or a bad value at pack time and lists every fault; a device never sees a broken manifest.
Bluetooth "connection failed" on a reflashed cardEvery installation now has its own Bluetooth name and address, so a computer that paired with an earlier card connects first time. A card from an older image kept the board's address: forget the arda-… device in the OS's Bluetooth settings once.
ssh to the device times outExternal SSH is closed — on every device, by design. Open it from the device's SSH tab; it closes itself when the time you chose runs out. The portal's own shell works either way.
The device came back with "Recovery"The last run ended in a power cut or a kill. Its stores are intact (every write is temp + fsync + rename); it is telling you, not asking for anything.

For AI assistants

The same guide is published as plain Markdown at ardaform.net/deployment.md, and the platform summary at /llms.txt. The facts an assistant needs most, in one block:

app folder     myapp/app/index.html (+assets), myapp/app/manifest.json (datasets, settings, files.needForBoot)
dev file       myapp/arda.dev.json beside app/ — never packed; keys: window scale bg console watch can screens device settings partition devices webhooks
settings page  myapp/gui/settings/index.html beside app/ — never sent to a device; shown in Setup › App settings and a device's Setup tab (paid plans); helper <script src="/_arda/gui.js"> (ArdaGUI)
run locally    ardaplayer -dev myapp/      (local; "device": true enrols the folder as a device)
headless       ardaplayer -headless myapp/ (no DOM; Arda.headless === true)
api            window.Arda — device() config db.dataset() db.push() db.status() on() update.* keyboard() screens.* screen[name] audio.* camera.* webhook.call() system.* wifi.* printers.* serial.*
deploy         GitHub App https://github.com/apps/ardaform → portal project → Code (branch) → Connectors (branch ← repo, folder, ref) → git push
images         portal Downloads: ardaplayer-rpi-arm64.img.xz · ardaplayer-linux-amd64.img.xz · ardaplayer-linux-amd64-installer.img.xz
wifi on a Pi   /boot/firmware/arda-wifi.conf: ssid=… psk=… [hidden=1]
adopt          activation code (Devices › Add device) · Devices › Adopt (same network) · Bluetooth (Chrome/Edge, arda-xxxx) · Install codes (XXXX-XXXX-XXXX, optional approval)
apply policy   per branch: asap (default) · timed HH:MM–HH:MM device time · manual; app: Arda.on('updateStart'), Arda.update.pause()/apply()
retire         portal Remove (wipe) · Forget completely (row deleted, wipe kept a year) · deleting the project (every device wiped on its next connection) · ardaplayer -unbind (device password)