JSON metadata template
The complete template for json/appname.json, every field the DevScripts sync reads, and the fields contributors leave out.
json/<slug>.json describes a script for the website: name, description, resources, notes and the values a user can pass to the install. It exists only in DevScripts. A workflow, push_json_to_pocketbase.yml, copies it into the website's database. This page documents exactly what that workflow reads.
The website's JSON generator produces the file from a form and can import an existing one.
Template
{
"name": "AppName",
"slug": "appname",
"categories": [0],
"date_created": "2026-01-16",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 3000,
"documentation": "https://docs.appname.com/",
"website": "https://appname.com/",
"repository": "https://github.com/owner/appname",
"architectures": ["amd64"],
"platforms": ["pve"],
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/appname.webp",
"description": "What the application is and what it can do, in two or three sentences.",
"install_methods": [
{
"type": "default",
"script": "ct/appname.sh",
"config_path": "/opt/appname/.env",
"resources": {
"cpu": 2,
"ram": 2048,
"hdd": 8,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Create the first account in the web interface; it becomes the administrator.",
"type": "info"
}
],
"app_vars": [
{
"name": "var_admin_user",
"label": "Admin Username",
"type": "text",
"default": "admin"
},
{
"name": "var_admin_token",
"label": "API Token",
"type": "password",
"secret": true,
"required": true,
"help": "The script exits when this is empty"
}
]
}Leave app_vars out when the install script takes no values from the user. A real file to compare with: json/temporal.json in DevScripts.
Top-level fields
| Field | Type | Meaning |
|---|---|---|
name | string | Display name on the website. |
slug | string | Lowercase identifier. Equals the file name: json/<slug>.json, ct/<slug>.sh. A file without slug is ignored by the sync. |
categories | array of numbers | One or more category ids from the table below. An id that is not in the table is dropped. |
date_created | string | The day you add the script, YYYY-MM-DD. |
type | string | ct for a container script, vm, addon (tools/addon/) or pve (tools/pve/). |
updateable | boolean | true when update_script updates the application. |
privileged | boolean | true when the container is privileged (var_unprivileged is 0). |
interface_port | number or null | Port of the web interface, null when there is none. |
documentation | string or null | Address of the upstream documentation. |
website | string or null | Address of the project's website. |
repository | string | Upstream repository as a full URL, for example https://github.com/owner/repo, https://gitlab.com/owner/repo or https://codeberg.org/owner/repo. Not owner/repo. |
architectures | array | ["amd64"], or ["amd64", "arm64"] when var_arm64 in the ct/ script is yes. Only amd64 and arm64 are accepted. |
platforms | array | ["pve"], or ["pve", "incus"] when the script was tested on Incus. Only pve and incus are accepted. |
logo | string | Address of the logo. Use the selfh.st icon set when the application is in it: https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/<slug>.webp. |
description | string | What the application is and does, two or three sentences, about 400 characters at most. Not what the script installs. |
install_methods | array | One entry per way to install, see below. |
default_credentials | object | username and password. Both null unless the application ships fixed credentials. A generated password is not a default credential; say in a note where it is stored. |
notes | array | Short hints for the user, see below. |
app_vars | array, optional | Values the install script accepts up front, see below. |
Always write architectures and platforms. If architectures is missing, the sync reads the var_arm64 line of the first install method's script instead: yes gives both architectures, anything else amd64 only. If platforms is missing, it stores ["pve"].
architectures describes; var_arm64 decides. The engine refuses to build on arm64 when the script says no, whatever the JSON claims. Keep them in agreement.
Categories
| Id | Category | Id | Category |
|---|---|---|---|
| 0 | Miscellaneous | 13 | Media & Streaming |
| 1 | Proxmox & Virtualization | 14 | *Arr Suite |
| 2 | Operating Systems | 15 | NVR & Cameras |
| 3 | Containers & Docker | 16 | IoT & Smart Home |
| 4 | Network & Firewall | 17 | ZigBee, Z-Wave & Matter |
| 5 | Adblock & DNS | 18 | MQTT & Messaging |
| 6 | Authentication & Security | 19 | Automation & Scheduling |
| 7 | Backup & Recovery | 20 | AI / Coding & Dev-Tools |
| 8 | Databases | 21 | Webservers & Proxies |
| 9 | Monitoring & Analytics | 22 | Bots & ChatOps |
| 10 | Dashboards & Frontends | 23 | Finance & Budgeting |
| 11 | Files & Downloads | 24 | Gaming & Leisure |
| 12 | Documents & Notes | 25 | Business & ERP |
The list is json/metadata.json in DevScripts.
Install methods
install_methods is stored as written. A script with one way to install has one entry.
| Field | Meaning |
|---|---|
type | default. A second entry with alpine describes an Alpine variant. |
script | Path of the script in the repository: ct/appname.sh. For other types vm/..., tools/addon/..., tools/pve/.... |
config_path | The application's main configuration file inside the container, or an empty string. It belongs here, not at the top level of the file. |
resources.cpu | Cores. Equals var_cpu. |
resources.ram | Memory in MB. Equals var_ram. |
resources.hdd | Disk in GB. Equals var_disk. |
resources.os | Distribution. Equals var_os. |
resources.version | Release of the distribution, as a string. Equals var_version. |
The sync also uses script, together with install/<slug>-install.sh, to find the last commit that touched the script.
Notes
"notes": [
{ "text": "Change the default password after first login!", "type": "warning" }
]type is info, warning or error. Use an empty array when there is nothing to say.
- At most three notes, one or two short sentences each.
- Only what a user has to know or do: how to get the first login, a required manual step, a real risk such as an interface without authentication.
- Not what the script installs, build times, memory figures, how updates work or upstream bugs.
app_vars
app_vars describes values the install script already reads from the environment, so the website's command generator can offer a field for each and an install can run without questions. It does not create the behaviour. The script has to read the variable and the ct/ script has to export it, see the install template.
| Field | Required | Meaning |
|---|---|---|
name | yes | The exact variable name, starting with var_. A name the script never reads produces a command that looks right and changes nothing. |
label | yes | Field label in the generator. |
type | yes | text, password, number, boolean (the value passed is yes or no) or select. |
default | no | Value used when the user enters nothing. The automated test uses it to answer the script. |
options | for select | Array of allowed values. |
required | no | true when the install cannot work without the value. |
secret | no | true for anything credential-like. The website keeps such values out of shareable links. |
help | no | Short help text shown with the field. |
A real example, from json/gitea-runner.json:
"app_vars": [
{
"name": "var_gitea_url",
"label": "Gitea Instance URL",
"type": "text",
"required": false,
"help": "URL of your Gitea instance (e.g. http://192.168.1.100:3000 or https://gitea.example.com)"
},
{
"name": "var_gitea_token",
"label": "Registration Token",
"type": "password",
"secret": true,
"required": false,
"help": "Runner registration token from Gitea (Site Administration > Actions > Runners or Repo/Org Settings > Actions > Runners)"
}
]The matching lines in ct/gitea-runner.sh:
export var_gitea_url="${var_gitea_url:-}"
export var_gitea_token="${var_gitea_token:-}"Fields you leave out
The sync fills these itself. Writing them has no effect or the wrong one:
| Website field | Where it comes from |
|---|---|
Development flag (is_dev) | Always set to true by the DevScripts sync. It is cleared when the script's pull request is merged in ProxmoxVE. |
Where the script runs (execute_in) | Derived from type. |
| Last update date and commit | The day of the sync and the last commit that touched the script files. |
These are owned by maintainers or by the website's own release sync. Do not add them to a new script's JSON:
| Field | Owner |
|---|---|
oidc_url | Maintainers. The sync writes it only when the key is present, so that a normal push never clears a link a maintainer set. |
version, changelog, screenshots | Filled from the upstream repository by the website. The sync would pass them through if present. |
pin_reason | A maintainer note for scripts that pin a version. The generator has a field for it, but the DevScripts sync does not read it. |
license | Set by maintainers in the database. The DevScripts sync does not read it. |
Older files may contain has_arm, script_created, port, default_user, default_passwd or github. The sync still understands them; do not use them in a new file.
When the file is read
| Event | Effect |
|---|---|
Your pull request is merged and the script's issue gets the Ready For Testing label | The record is created and the script appears on the website as a development script. |
A later change to the file reaches main | The existing record is updated. A push alone never creates a record. |
| The script is promoted to ProxmoxVE | The record is switched to released and the file is deleted from DevScripts. From then on metadata is changed in the database, through the "Report Issue" link on the script's page or by an organisation member with the /pocketbase command. |
Once the record is marked as released, the DevScripts sync skips it.