Manifest reference

Last updated 2026-09-21

manifest.json sits at the root of the .plu and declares what the app is and what it may do. The box parses it (internal/apps/manifest.go), the store validates it on upload, and plum-dev validate checks the same rules locally against manifest.schema.json.

At most 32 KiB. Unknown keys are preserved, not rejected.

Fields

Field Type Required Rules
id string yes ^[a-z0-9][a-z0-9._-]{0,127}$, and inside a namespace you own
name string yes 1–80 characters
version string yes non-empty, 1–40 characters. The box does not parse it; the store requires each upload to be higher than the last (dotted numeric comparison), so use semver
entry string no HTML entry point, default index.html; must exist in the bundle
icon string no bundle-relative image; must exist in the bundle
description string no shown in listings
mimeTypes string[] no file types this app opens (see below); not validated
permissions string[] no any of files:read, files:write, user:profile, service:call; unique
mobile boolean no default false. true shows the app in the Plum mobile app's launchpad, full-screen. Leave it off for apps that assume a pointer and a wide viewport
server object no a box-side service — see Service apps
clients object[] no companion OAuth clients — see Companion apps
author string no not part of the schema, but the box passes it through to GET /api/apps/overview
protocols object[] no standard-protocol mounts your service answers — core 1.11+

protocols[] (core 1.11+)

A service app can answer WebDAV, CalDAV or CardDAV on a path of its own, and the box proxies that path to it unchanged. Each entry is {"type": "webdav" | "caldav" | "carddav", "mount": "/dav/<your app id>/"}, optionally with one extra segment: /dav/com.acme.notes/calendar/. At most four entries, no repeats. The app id is the namespace, so no app can claim another's mount or the box's own /dav/files/. It requires server and the service:call permission — a mount with nothing behind it is just a 503 generator.

Requests arrive with the path untouched (so <D:href> and Destination: line up) plus a X-Plum-Mount header telling you the prefix to build hrefs from. Authentication is a personal access token scoped service:call:<your app id>, sent as a Bearer token or as the password in HTTP Basic; a session cookie is never accepted on a mount. See Service apps.

Not accepted today: pricing. It appears in the platform design but not in the shipped core; paid features are read-only entitlements, not a manifest field.

permissions

Four strings, nothing else:

Permission Grants
files:read read the user's files through handles, the picker, and the control socket
files:write write and publish files
user:profile plum.user.current() and the control socket's /user
service:call the app's own panel may call its own backend at /apps/<id>/svc/*

Permissions are declared here and can be switched off by the user per app; the box enforces the effective set (GET /api/apps/{id}/permissions). A panel that calls something it did not declare gets PermissionDeniedError.

For OAuth, the same words appear as scopes, with service:call:<app_id> naming the app explicitly. read and write are accepted as aliases (write implies both read and write).

mimeTypes

Extensions (".md" or "md"), exact MIME types ("text/markdown") and wildcards ("image/*") all work. Declaring them puts the app in Drive's Open with menu; the box then launches it with a file handle, which the app reads with plum.files.launchFile().

server

"server": {
  "bin": "svc",
  "args": [],
  "healthPath": "/healthz",
  "limits": { "memory": "256M", "cpu": 50, "pids": 64 }
}
Field Rules
bin required; a safe relative path inside the bundle; 64-bit arm64 ELF
args optional argv
healthPath default /healthz; a status below 500 means ready
limits.memory 256M, 1G, 512MiB… Default 256 MiB, clamped to 32 MiB–1 GiB
limits.cpu percent of one core (100 = one core). Default 50, clamped to 5–200
limits.pids Default 64, clamped to 8–256

The manifest schema accepts cpu up to 400 and pids up to 1024, but the box clamps to the values above, so declaring more does not get you more.

clients

Up to 10 entries. Each registers a native client that may ask a box for delegated access under your app id:

"clients": [{
  "client_id": "dev.jan.notes:ios",
  "display_name": "Notes for iPhone",
  "platform": "ios",
  "redirect_uris": ["notes://auth/callback", "http://127.0.0.1/callback"],
  "scopes_allowed": ["files:read", "files:write", "service:call:dev.jan.notes"]
}]
Field Rules
client_id <your manifest id>:<label>, label ^[a-z0-9-]{1,32}$
display_name 1–80 characters; this is what the consent screen shows
platform ios, android, macos, windows, linux, web or cli
redirect_uris 1–8 entries, each ≤ 512 characters. A custom scheme, or http loopback (127.0.0.1, ::1, localhost, any port). https is refused
scopes_allowed 1–8 entries from files:read, files:write, user:profile, service:call:<your app id> — naming another app's service is an error

The store registers manifest clients on upload; a paired box also honours them straight from the installed manifest, so you can test a companion app before publishing anything.

A complete example

{
  "id": "dev.jan.notes",
  "name": "Notes",
  "version": "1.2.0",
  "entry": "index.html",
  "icon": "icon.png",
  "description": "Plain-text notes that stay on your box.",
  "author": "Jan",
  "mobile": true,
  "mimeTypes": [".md", "text/markdown"],
  "permissions": ["files:read", "files:write", "user:profile", "service:call"],
  "server": {
    "bin": "svc",
    "healthPath": "/healthz",
    "limits": { "memory": "128M", "cpu": 50, "pids": 32 }
  },
  "clients": [{
    "client_id": "dev.jan.notes:ios",
    "display_name": "Notes for iPhone",
    "platform": "ios",
    "redirect_uris": ["notes://auth/callback"],
    "scopes_allowed": ["files:read", "files:write", "service:call:dev.jan.notes"]
  }]
}

Bundle limits

The .plu itself is checked too: at most 2000 files, 32 MiB per file, 200 MiB uncompressed, and 50 MiB for the archive when installing directly on a box (the store accepts up to 64 MiB). No symlinks, no device nodes, no absolute paths, no ... plum-dev validate reports all of it before you upload.