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
- Panel SDK — everything
window.plumcan do. - Service apps — add a Go backend that runs on the box.
- Manifest reference — permissions, mime types, limits.
- Publishing — when you want it on other people's boxes.