The .arda
package.

You never build one. ArdaForm packs your commit into a sealed container when it deploys, hands the key to each device inside a signed instruction, and the device opens it in memory. This page is what that container is, what goes in, what is refused, and the order the runtime checks it in. Getting started? You can skip it — packing happens for you on every push.

Working — this is the format on deployed devices

Why a package at all

The alternative is a directory of files on the device's disk. That is fine while you own the machine. It stops being fine the moment the machine sits on a customer's counter, because a directory of JavaScript is a directory anyone with a screwdriver can copy, read and change.

A package gives you two things a directory can't: confidentiality — your application is ciphertext on the card, and only a device that was handed the key by the control plane can open it — and proof of what is running: every device reports the exact commit it holds, and a container whose bytes differ from the signed instruction that named it never opens.

Who packs, and when

ArdaForm packs; ArdaForm never runs your code. An application is plain HTML, CSS and JavaScript, so turning a folder into a container is a file operation. You develop locally and push to your own GitHub repository; when a branch's connection points at a new commit, the control plane fetches that commit, collects the folder, packs it once, and offers the result to the devices following that branch.

  • Nothing from the repository is executed — no install step, no build script, no package manager. A build output you want on the device is a build output you commit.
  • One pack per commit and source, cached. Two devices on the same branch receive byte-identical containers, so they can be compared by hash.
  • The manifest inside the folder (app/manifest.json) is reconciled at pack time: missing datasets and setting fields are created and definitions updated before any device is told. A manifest with a fault fails the pack and every fault is reported; nothing reaches a device.

What goes in

Everything under the connected folder — the one whose index.html is the app's root, usually app/ — byte for byte, with these exceptions. They are refusals rather than warnings because a container is what a shop runs.

RuleEffect
A symlink anywhereRefuses the whole pack. A link can point outside the folder, and the packer will not follow it to find out where.
Dotfiles, .env*, .git, .githubNever packed. A credential committed by accident does not reach a device.
arda.dev.jsonNever packed, by name. It is your desk's settings, and a device honouring one would be taking instructions from an app folder.
More than 4,096 files or 64 MiBRefused while the folder is being collected, not after. Media libraries belong in the project's file store, which syncs separately and never blocks a code update.

Structure

The container is the folder's files as a tar stream, gzipped, then encrypted as one AES-256-GCM message with the additional data ARDAAPP1 binding the format version. There is no cleartext manifest, no per-file table and no readable index: the only thing a disk yields without the key is the format header and a length.

layout
┌─────────────────────────────────────────────┐
│ "ARDAAPP1" ‖ 0x01   (9-byte header)           │  cleartext
├─────────────────────────────────────────────┤
│ nonce                                         │  cleartext
├─────────────────────────────────────────────┤
│ aes-256-gcm( gzip( tar( app/ ) ) )            │  ciphertext
│   AAD = "ARDAAPP1" ‖ 0x01                     │
│   tag = 16 bytes, checked on open             │
└─────────────────────────────────────────────┘

On the device the container is opened into memory behind a virtual filesystem the app server reads from. Nothing decrypted ever touches the card, so there is no window during which the application exists as readable files on the device, and no clean-up that could be skipped by a power cut.

How the key travels

The content key rides inside the update instruction the control plane sends to each device — an Ed25519-signed message that also names the container's SHA-256, its version and the device it is for. The device stores the key sealed under its own hardware-bound derivation, beside everything else it keeps, so a cold boot serves the app with no network at all.

  • The control plane keeps the key beside its cached copy of the container on its own disk, so a device added later gets the same container and the same key without a re-pack. The key leaves the control plane only inside a signed instruction. There is no key for you to generate, store or lose.
  • The device id is inside the signed bytes, so an instruction captured from one device is useless at another.
  • The instruction is durable and queued: a device that was unplugged during a deploy receives it the moment it reconnects.
!
What the signature proves The packer is on the server, so the signature establishes that ArdaForm produced this container for your project from the commit it names and that it arrived unaltered — not that you signed it on your own machine. If your situation needs the second guarantee, say so; it is a different design and we would rather build it than imply it.

Verification order

Every check must pass before anything is moved. Failure at any step leaves the running application untouched — a bad package cannot take a device out of service.

on the device
1  the instruction's Ed25519 signature is valid and names THIS device
2  the instruction has not expired (judged against server time, never the device clock)
3  the bytes fetched match the SHA-256 the instruction named
4  the container opens — the AES-GCM tag verifies with the key the instruction carried
5  it contains no arda.dev.json
6  staged to application/next/, and a READY marker is written LAST
7  applied when the app allows, at the next start, or when an operator says so

The outcome of each instruction is reported back, so a deploy that fails on some devices tells you why for each one rather than merely that.

Staging and applying

A verified container is written beside the running one, and the marker that says "complete" is the last thing written. A power cut mid-download leaves files with no marker, which the next start clears rather than running half an app. Applying proves the container opens before moving anything, removes the marker first and then swaps — from the instant the marker is gone, the move is the only truth, and it is safe to repeat.

When it applies is the app's call under the branch's policy: told through Arda.on('updateStart'), an app that says nothing is reloaded five seconds later; one that calls Arda.update.pause() gets a minute, up to five times; one that calls Arda.update.apply() reloads on its own terms. A restart applies whatever is staged, except on a branch set to apply only when the app asks. See the deployment guide.

Run it, never debug it

One rule shapes where a container may run: whoever holds a package can run it, never debug it.

  • A loose app/index.html runs only in developer mode and headless mode. A device in kiosk mode refuses loose files — its app arrives as a container or not at all.
  • A container is refused by developer mode, before any file beside it is read. There is no way to open a deployed package in a window with an inspector on a desk.
  • The portal's console and screenshot reach a deployed app through the device's own signed session, because the operator there is the tenant who owns both the app and the device.

Limits

LimitValue
Files per package4,096
Package size, before compression64 MiB

If you are approaching either with media, put the media in the project's Files tab instead. It syncs to devices on its own driver, is served to the app under /storage/, and a changed picture never makes every device download the application again.