Manifest reference
Every plugin has a holdall.plugin.json file at the root of its archive. Holdall reads it before anything else, refuses the archive if it is invalid, and shows the person installing it exactly what it says.
{ "id": "com.example.notes-word-count", "name": "Word count", "version": "1.0.0", "type": "extension", "description": "Shows how many words you wrote today.", "author": { "name": "Example Dev", "url": "https://github.com/example" }, "license": "MIT", "holdall": ">=0.1.0", "permissions": ["ui:slots", "storage", "notes:read", "hooks:notes.saved"], "optionalPermissions": ["network", "notifications"], "network": ["api.example.com"], "entry": { "server": "dist/server.mjs", "ui": "ui/page.html" }, "contributes": { "hooks": ["notes.saved"], "widgets": [{ "id": "today", "title": "Words today", "entry": "ui/widget.html", "width": 1 }], "page": { "title": "Word count", "entry": "ui/page.html" } }}Fields
Section titled “Fields”| Field | Required | Rules | Meaning |
|---|---|---|---|
id |
yes | Reverse domain name in lower case, for example io.github.you.word-count |
Unique id. Never change it: Holdall uses it to recognize updates and keep data. |
name |
yes | 1 to 60 characters | Shown in the store, the consent screen and notifications. |
version |
yes | Semantic version, like 1.2.0 |
Raise it for every release. |
type |
yes | module, extension, theme or library |
See types. |
description |
no | Up to 300 characters | One sentence about what it does. |
author |
yes | name (up to 80 characters), optional url |
Who made it. |
license |
yes | Up to 40 characters, for example MIT |
The license of your code. |
holdall |
yes | >=x.y.z, ^x.y.z, ~x.y.z, an exact version or * |
Holdall versions it works with. Install is refused on others. |
permissions |
no | Known permissions, up to 40 | Required. Refusing one cancels the install. See Permissions. |
optionalPermissions |
no | Known permissions, up to 40 | The admin can uncheck them at install and change them later. |
network |
no | Plain host names, up to 20 | Hosts network.fetch may call with the network permission. Not needed with network:any or network:local. |
dependencies |
no | { "plugin.id": "version range" } |
Plugins that must be installed first, in a matching version. Your server code can call the libraries listed here. |
entry.server |
no | Relative path to a file in the archive | Server code. See Server code and hooks. |
entry.ui |
no | Relative path | Main interface file. |
theme |
theme only | Relative path | Theme file. See Themes. |
contributes.hooks |
no | Known hook names | Hooks your server code listens to. Each needs its hooks:<name> permission. |
contributes.widgets |
no | Up to 10 | Widgets for Today. See below. |
contributes.page |
no | title (up to 40 characters), entry, optional icon and pages |
A page in the side bar, with sub pages. See Page. |
contributes.settings |
no | Up to 40 fields | A settings form drawn by Holdall. See Settings. |
Unknown fields are refused, so a typo is caught at install instead of being ignored.
Widgets
Section titled “Widgets”{ "id": "today", "title": "Words today", "entry": "ui/widget.html", "width": 1 }id: lower case letters, digits and dashes, unique inside your plugin.title: the heading Holdall draws above the widget. People can rename it in Customize.entry: the HTML file.width:1,2or3columns of the Today grid. On a phone every widget takes the full width.
Widgets need the ui:slots permission. They appear on Today for each person who turns the plugin on; people can move and hide them in Customize.
{ "title": "TV", "entry": "ui/page.html", "icon": "ui/icon.svg", "pages": [ { "title": "Live", "path": "live" }, { "title": "Saved", "path": "saved" } ]}- The page gets an entry in the side bar, named
title, and opens at/p/<plugin id>. It fills the content area. It needs theui:slotspermission. icon: an.svgfile of your archive. Holdall draws it in one color, the color of the side bar, so it matches every theme. Use simple filled shapes on a 16 by 16 grid.pages: up to 20 sub pages. When someone opens your page, they unfold under it in the side bar. Each one opens/p/<plugin id>/<path>, with the sameentryfile.pathuses letters, digits and. _ ~ -, with/between parts.- Any address under
/p/<plugin id>/opens your page, not only the listed ones, so you can give each item its own shareable address, like/p/com.example.tv/channels/42. See Pages and addresses.
Settings
Section titled “Settings”Declare your settings and Holdall draws the form, validates the values, encrypts secrets and stores everything. A Settings button appears next to your plugin on the Plugins page.
"settings": [ { "key": "apiKey", "label": "API key", "type": "secret", "scope": "global", "description": "From your weather provider account.", "required": true }, { "key": "city", "label": "City", "type": "text", "scope": "personal", "default": "Paris" }, { "key": "units", "label": "Units", "type": "select", "scope": "personal", "options": [{ "value": "c", "label": "Celsius" }, { "value": "f", "label": "Fahrenheit" }] }, { "key": "days", "label": "Days to show", "type": "number", "scope": "personal", "min": 1, "max": 7, "default": 3 }, { "key": "alerts", "label": "Show alerts", "type": "boolean", "scope": "personal" }]| Field | Meaning |
|---|---|
key |
Name of the value, a letter then letters, digits or _, up to 40 characters. Unique. |
label |
What people read, up to 60 characters. |
description |
Help under the field, up to 300 characters. |
type |
text, textarea, number, boolean, select, url or secret. |
scope |
personal (default): each person sets their own. global: one value for everyone, set by admins. |
default |
Value used until someone saves one. Not used for secret. |
options |
For select: the list of { value, label }. Required for that type. |
min, max |
For number. |
required |
The form refuses an empty value. |
secretvalues (API keys, passwords) are encrypted at rest withAPP_SECRET. The form never shows them again; leaving the field empty keeps the saved value.- Only your server code can read a secret. Your interface gets
truewhen it is set andnullwhen it is not, so a key never reaches the browser. - Personal settings are deleted with the person’s account; all settings are deleted when the plugin is uninstalled.
Read them with settings.get.
Rules checked at install
Section titled “Rules checked at install”id,versionandholdallare valid, and this Holdall matchesholdall.- Every plugin in
dependenciesis installed, in a matching version. - Every file named in the manifest is in the archive, with a relative path that stays inside it.
- Every hook in
contributes.hookshas the matchinghooks:<name>permission. networkis only used with thenetwork,network:anyornetwork:localpermission.- A theme plugin has a valid
themefile and noentry. - The archive is at most 20 MB, and 60 MB and 500 files once unpacked.
Install errors name the problem, for example: Word count needs Holdall >=0.2.0. This Holdall is 0.1.0.