Your first panel app

Last updated 2026-09-21

A panel is a web app that the box serves and the Plum shell shows. It is the shortest path onto a box: no native toolchain, no build step, one script tag.

You need Node 18 or newer. A box is optional until step 5 — and if you do not have one, the emulator stands in for it.

1. Install the CLI

npm i -g @plumbox/dev
plum-dev --version        # 0.1.0

Node 18 or newer. To run an unreleased change instead, build the CLI from the repository: git clone https://github.com/plum-networks/plum-sdk, then cd plum-sdk/packages/cli && npm i && npm run build && npm link.

2. Make your keys

plum-dev keygen

Two Ed25519 keys land in ~/.config/plum-dev/: the signing key you use every day and a recovery key to keep offline. The command prints both public keys and their kid. Nothing unsigned ever runs on a box, and this is the key that signs. See Signing and trust.

3. Scaffold the app

plum-dev init my-notes --template panel
cd my-notes

You get a manifest, an index.html that loads the SDK, a .plumignore and a README. The app id defaults to your namespace plus the name, e.g. dev.jan.my-notes.

{
  "id": "dev.jan.my-notes",
  "name": "my-notes",
  "version": "0.1.0",
  "entry": "index.html",
  "description": "my-notes for Plum Box",
  "mobile": true,
  "permissions": ["user:profile"]
}

4. Run it locally

plum-dev serve
# http://127.0.0.1:4040/apps/dev.jan.my-notes/

serve injects the mock SDK, so plum.user.current(), plum.files.openPicker() and friends answer with fixtures and no box is involved. Edit, reload, repeat. (If your app also has a backend, --service http://127.0.0.1:8080 proxies /apps/<id>/svc/* to it and fakes the identity headers — see Service apps.)

Write something that uses the SDK:

<script src="/apps/runtime/plum-sdk.js"></script>
<script type="module">
  const me = await plum.user.current();     // needs "user:profile"
  document.title = `Notes — ${me.displayName}`;
</script>

5. Pair with your box

Your box only runs code signed by a key it trusts, and you add your key by pairing:

plum-dev pair https://pb-1234.plumbox.me --namespace dev.jan.

The box shows a 6-digit code to its owner in Settings › Developer (it expires in five minutes). Type it into the CLI. The box then trusts your key for app ids under your namespace, and hands the CLI a token with the apps:install and apps:dev scopes so it can install and read logs. On a LAN box with a self-signed certificate, add --insecure.

6. Push it

plum-dev push

push validates the app, signs it in memory, and installs it over POST /api/apps/install. No store, no internet: this works on a LAN with the cloud unplugged. Open the app from the launchpad in the box's web UI or in the Plum app.

While you work:

plum-dev push --watch

Every change re-pushes, and the page reloads itself — a developer build is served with Cache-Control: no-store, and the SDK reloads when the box publishes the apps:<id> {"type":"installed"} event.

plum-dev push --logs follows the service log afterwards; for a panel with no server in its manifest it just says there is nothing to follow.

What the box records

A directly installed app has your publisher key and no store countersignature. The box keeps both facts (GET /api/apps/overview returns publisherKid and countersignKind) and leaves such an app out of automatic updates. A developer build badge in the launchpad UI is planned, not shipped.

Useful next commands:

plum-dev status dev.jan.my-notes
plum-dev logs dev.jan.my-notes -f
plum-dev restart dev.jan.my-notes
plum-dev uninstall dev.jan.my-notes

Next