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.
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.
┌─────────────────────────────────────────────┐
│ 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Application identifier. Lowercase, hyphenated. |
version | string | yes | Semantic version. Informational — releases are identified by ID and hash. |
entry | string | yes | Path to the entry HTML, relative to root. |
root | string | no | Directory to package. Defaults to the project root. |
build | string | no | Shell command run before packaging. |
capabilities | string[] | no | Device capabilities the application may use. Empty means none. |
bind | string | no | device, installation, account or none. Default account. |
kiosk | object | no | Surface options — cursor visibility, idle blanking, orientation. |
ignore | string[] | no | Glob patterns excluded from the bundle. |
fleet | string | no | Default fleet, written by arda link. |
channel | string | no | Default channel for arda push. |
"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.
| Capability | Grants |
|---|---|
db | arda.db() — encrypted local key/value stores. |
storage | arda.storage — scoped file storage. |
sync | arda.sync — the server bridge. Without it the app is fully offline. |
net:status | Read connectivity and interface state. |
net:wifi | Scan and join Wi-Fi networks, store credentials. |
usb:read | Enumerate USB devices and receive attach/detach events. |
printer | Discover and print, including raw ESC/POS and drawer control. |
camera | Open cameras, capture stills and scan codes. |
face | Local face detection, enrolment and matching. |
gpio | Digital I/O on boards that expose it. |
audio | Device-level playback and volume control. |
system:power | arda.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:
- 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.
- 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.
| Mode | Runs on | Use when |
|---|---|---|
none | Any device with the right key | Development, demos, internal builds. |
account | Any device registered to your account | The usual choice. Devices can be swapped without repackaging. |
installation | Devices registered to a named installation or customer | Per-customer licensing, where a package must not move between customers. |
device | One specific device ID | High-value deployments where a copied package must be worthless. |
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.
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 → loadThe 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 keygenand 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
| Limit | Value |
|---|---|
| Maximum package size | 256 MB |
| Maximum files per package | 65,535 |
| Manifest size | 64 KB |
| Capabilities per package | 32 |
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.