The .arda
package.

One file containing your entire application, signed so a device can prove where it came from and encrypted so the machine it sits on can't read it. This page describes what's inside and the order the runtime checks it.

Working — implemented and in production

Why a package at all

The alternative is a directory of files on the device's disk. That's fine when you own the machine. It stops being fine the moment the machine is on a customer's counter, because a directory of JavaScript is a directory of JavaScript — readable, copyable and modifiable by anyone with ten minutes and a USB stick.

A package gives you three things a directory can't: proof of origin (nobody can substitute a modified application), confidentiality (your business logic isn't sitting in plain text on hardware you don't control), and binding (a copied package won't run somewhere you didn't authorise).

Structure

A .arda file is a container with a small cleartext header followed by an encrypted payload. The header carries only what the runtime needs to decide whether to proceed — never anything about your application's contents.

layout
┌─────────────────────────────────────────────┐
 magic          "ARDA" + format version          cleartext
 header         key id · binding mode · nonce   cleartext
 signature      ed25519 over header+payload     cleartext
├─────────────────────────────────────────────┤
 payload        aes-256-gcm                   
   ├── manifest    arda.json                    encrypted
   ├── index.html                                encrypted
   ├── assets/…                                  encrypted
   └── integrity    per-file digests             encrypted
└─────────────────────────────────────────────┘

The payload is decrypted into memory at load. It is not unpacked to disk, so there is no window during which your application exists as readable files on the device.

Manifest schema

arda.json lives at the root of your project and is embedded in the package at build time.

FieldTypeRequiredMeaning
namestringyesApplication identifier. Lowercase, hyphenated.
versionstringyesSemantic version. Informational — releases are identified by ID and hash.
entrystringyesPath to the entry HTML, relative to root.
rootstringnoDirectory to package. Defaults to the project root.
buildstringnoShell command run before packaging.
capabilitiesstring[]noDevice capabilities the application may use. Empty means none.
bindstringnodevice, installation, account or none. Default account.
kioskobjectnoSurface options — cursor visibility, idle blanking, orientation.
ignorestring[]noGlob patterns excluded from the bundle.
fleetstringnoDefault fleet, written by arda link.
channelstringnoDefault channel for arda push.
!
Exclude your source maps "ignore": ["*.map"] should be in almost every manifest. Encrypting a bundle and then shipping the source map beside it defeats the exercise.

Capabilities

Capabilities are declared at build time and enforced at load. An undeclared group is not present on the arda object at all — not a method that throws, but undefined. That distinction matters: a bug or a compromised dependency cannot call something that doesn't exist.

CapabilityGrants
dbarda.db() — encrypted local key/value stores.
storagearda.storage — scoped file storage.
syncarda.sync — the server bridge. Without it the app is fully offline.
net:statusRead connectivity and interface state.
net:wifiScan and join Wi-Fi networks, store credentials.
usb:readEnumerate USB devices and receive attach/detach events.
printerDiscover and print, including raw ESC/POS and drawer control.
cameraOpen cameras, capture stills and scan codes.
faceLocal face detection, enrolment and matching.
gpioDigital I/O on boards that expose it.
audioDevice-level playback and volume control.
system:powerarda.system.reboot(). Deliberately separate from the rest of system.

arda.system.info(), arda.system.log() and arda.app are always available and need no capability.

Signing and encryption

Two operations, in this order, both performed on your machine or your CI runner:

  1. Encrypt — the payload is encrypted with AES-256-GCM under a content key. The GCM authentication tag means any modification to the ciphertext is detected on decryption.
  2. Sign — an Ed25519 signature is computed over the header and the encrypted payload with your pack key. This proves the package came from someone holding that key.

The control plane stores and distributes the resulting bytes. It never holds the private half of your pack key and therefore cannot produce a package your devices would accept.

Device binding

The bind field decides how portable the package is. Binding is enforced by the runtime at load, using values derived from the machine rather than supplied to it.

ModeRuns onUse when
noneAny device with the right keyDevelopment, demos, internal builds.
accountAny device registered to your accountThe usual choice. Devices can be swapped without repackaging.
installationDevices registered to a named installation or customerPer-customer licensing, where a package must not move between customers.
deviceOne specific device IDHigh-value deployments where a copied package must be worthless.
i
Trade-off Tighter binding means more repackaging when hardware changes. device binding is genuinely strict: replace a failed motherboard and the device ID changes, so the package must be reissued. Most fleets want account or installation.

Verification order

Every check must pass before the application is allowed to run. Failure at any step means the package is discarded and the previously staged release keeps running — a bad package cannot take a device out of service.

on the device
1  magic and format version recognised           else UNSUPPORTED_FORMAT
2  SHA-256 matches the update instruction        else HASH_MISMATCH
3  Ed25519 signature valid for the key id        else BAD_SIGNATURE
4  binding mode satisfied by this device         else NOT_BOUND
5  AES-GCM tag verifies on decrypt               else DECRYPT_FAILED
6  manifest parses and entry file present        else BAD_MANIFEST
7  per-file integrity digests match              else INTEGRITY_FAILED
8  capabilities resolved and bridge scoped       load

The failure code is reported back to the control plane in the update status, so a rollout that fails on a subset of devices tells you why rather than merely that.

Key management

Your pack key is the thing that matters. Treat it like a code-signing certificate, because that is what it is.

  • Generate it once with arda keygen and store the private half in your CI secret store.
  • Never commit it. A pack key in a repository is a pack key on someone else's laptop.
  • Register the public half with your fleet so devices know which key to trust.
  • If it leaks, generate a new one, register it, re-sign and deploy. The old key can be revoked from the fleet so devices stop accepting packages signed with it — but packages already signed and already installed are not retroactively invalidated.

A separate release key, held by the ArdaPlayer release pipeline rather than by you, signs runtime binary updates. The two are independent by design: neither can be used to forge the other's artefacts.

Limits

LimitValue
Maximum package size256 MB
Maximum files per package65,535
Manifest size64 KB
Capabilities per package32

If you are approaching the size limit with media, put the media in device storage and sync it separately — bundling a gigabyte of video into every release makes every update a gigabyte download on a shop's broadband.