NX Addons

Writing an NX addon

An addon is your own program that works beside an NX panel: its own service or container, its own database, talking to the panel only over the network with the permissions the operator approved. This page walks through what an addon needs, from the first run to a listing here.

What an addon is

The panel never runs an addon's code. The operator installs the addon on a host — the panel's own or another — and the host's systemd or Docker keeps it running. The panel then registers it: it reads the addon's manifest, shows the operator what it asks for, and on approval hands it an API token and a signed event subscription.

  • Write it in any language: the contract is HTTP and JSON. In Go, addon-kit does the contract for you.
  • Keep your data in your own database. The panel stores nothing of an addon's, and an addon backs itself up.
  • Ask for the fewest permissions that do the job: the operator reads each one, with your reason, before approving.

Start from the template

addon-template is a complete minimal addon: it registers, hears events, answers health and shows a page, with an install script, a compose file and a release workflow. Copy it and run it:

gh repo create my-addon --template nx-panel/addon-template --public --clone
cd my-addon
NX_OPT_PORT=8090 go run .
# not registered yet — claim code 3F9A1C0B7E21

A panel registers by claim code only a manifest signed with a key it trusts. To try yours against a panel built with -tags addondev, sign it with the development key, which only such a panel trusts:

go run github.com/nx-panel/addon-kit/cmd/nx-addon sign -dev nx-addon.json > signed.json
NX_MANIFEST_FILE=signed.json go run .

The manifest

nx-addon.json lives in two places: at the root of your public repository, where this directory and the panel's install wizard read it before anything runs, and served by the running addon at /.well-known/nx-addon.json, which registration reads. The two must be the same file. SPEC.md is the reference for every field; a working example:

{
  "api": 1,
  "slug": "hello",
  "name": "Hello",
  "version": "0.1.0",
  "publisher": "Your name",
  "license": "Apache-2.0",
  "description": { "en": "What it does, in a sentence or two." },
  "docs": "https://github.com/you/hello",
  "install": {
    "docker": { "image": "ghcr.io/you/hello", "compose": "deploy/compose.yml" },
    "binary": { "linux-amd64": "hello-linux-amd64.tar.gz", "linux-arm64": "hello-linux-arm64.tar.gz" },
    "options": [
      { "key": "port", "type": "port", "default": 8090, "label": { "en": "Port" } }
    ]
  },
  "scopes": [ { "scope": "users:read", "purpose": "to show how many accounts the panel has" } ],
  "events": ["user.created", "panel.addon_removed"],
  "webhook": "/nx/events",
  "setup": "/nx/setup",
  "health": "/health",
  "ui": "/"
}
  • slug is the addon's identity: in the directory, in the panel and in update checks. Never change it; a new slug is a new addon.
  • version is the release without the v; panels compare it to show the update badge.
  • scopes are the API token's permissions, each with the reason the consent screen shows; events are delivered to webhook. Ask for one or both.
  • description and every label take en, fa, ru and zh; English is required, the panel shows the operator's language when you give it.
  • Check it the way the panel will: nx-addon check nx-addon.json.

Install options

install.options are the questions the panel's wizard asks before installing — a port, a database, a bot token. Types: string, secret, password (a secret that becomes the addon's admin password: at least 10 characters, at most 72 bytes), number, port, bool, choice, url, path (the base path the addon serves its admin under: one segment, or empty for the root; at most one per manifest). An option with when is asked only while an earlier choice or bool has that value.

{ "key": "database", "type": "choice", "choices": ["sqlite", "postgres"], "default": "sqlite",
  "label": { "en": "Database" } },
{ "key": "database_dsn", "type": "secret", "required": true, "when": { "database": "postgres" },
  "label": { "en": "PostgreSQL DSN" } }

# reaches the addon as
NX_OPT_DATABASE=postgres
NX_OPT_DATABASE_DSN=postgres://…
NX_PANEL_URL=https://panel.example
NX_CLAIM_CODE=K7QF-2M9D-XW4H-8RTA

Every answer reaches the addon as an environment variable NX_OPT_<KEY>, beside the panel's address and the claim code the panel issued for this install. A secret is never stored by the panel: it is in the command shown once, or passed over SSH.

Releases and install.sh

Both install paths — the command the panel prints, and the panel installing over SSH — run your install.sh from your latest GitHub release. The template's script already does all of this; keep its flags:

sh install.sh --method script|docker   # binary + systemd, or the compose stack
           --version v0.1.0               # a release tag (default: the latest)
           --binary-file FILE             # use this archive instead of downloading it
           --opt KEY=VALUE                # an answer to an install option; repeat
           --panel-url URL                # → NX_PANEL_URL
           --claim-code CODE              # → NX_CLAIM_CODE
sh install.sh                             # again: update in place, answers and data kept
sh install.sh --uninstall [--purge]       # remove; --purge deletes the data too

What a release must carry, all at the same tag:

nx-addon.json             the manifest, its version = the tag
install.sh                    at the root of the repository, at the tag
deploy/compose.yml            what "install.docker.compose" names
hello-linux-amd64.tar.gz      every asset "install.binary" names (release assets)
hello-linux-arm64.tar.gz
SHA256SUMS                    sha256sum output for the assets — the panel checks it
ghcr.io/you/hello:0.1.0       the image, tagged with the version

For a script install over SSH the panel downloads the binary for the host's architecture itself, checks it against SHA256SUMS and uploads it with --binary-file, so the host needs no access to GitHub. Run as root; keep everything under /opt/nx-addons/<slug>, the answers in its .env and the data in its data/. Running the script again must update in place.

Registration, events and health

When the operator approves, the panel POSTs the credentials to your setup path, with the claim code. Accept them only with the code you were started with (or drew and logged); then forget the code. Keep the token and the webhook secret in a file only you can read.

POST /nx/setup
{
  "api": 1,
  "claimCode": "K7QF-2M9D-XW4H-8RTA",
  "panel": { "url": "https://panel.example", "version": "1.8.0", "id": "…" },
  "token": "nx_…",
  "scopes": ["users:read"],
  "webhook": { "id": 7, "url": "…/nx/events", "events": ["user.created"], "secret": "…" }
}

Events arrive at your webhook path, signed:

X-Nx-Delivery: 9b1f…            # the same on every retry: handle it once
X-Nx-Signature: t=1791000000,v1=<hex>
# v1 = HMAC-SHA256(secret, "<t>.<body>"); refuse a t more than 5 minutes off

Your health path is asked every minute and must answer 200 when you are well; a failure shows on the operator's Addons page. Call the panel with your token under /api/v1; send an Idempotency-Key with every write so a retry never acts twice:

GET https://panel.example/api/v1/users?limit=1
Authorization: Bearer nx_…
Idempotency-Key: <one per logical write>   # on POST / PUT / PATCH / DELETE

When the operator removes the addon, it hears panel.addon_removed before its token is deleted: forget the credentials so it can be registered again. In Go, addon.New, Mount, OnEvent and Forget are all of this.

Official, verified, unofficial

TierListed byManifest signed byIn the panel
OfficialNXNX's keyinstalled by command or automatically over SSH; consent screen says NX
Verifiedyou, reviewed by NXyour own key, which the signed index vouches foras official, labelled with your name
Unofficialyou, after a light checkyour key, or nonea warning on the card and the consent screen; installed by command only

A manifest signed by a key the directory does not vouch for counts as unsigned; an unsigned addon is registered only by hand, with the operator creating its token.

Getting listed

  1. Make the repository public, with the manifest at its root and at least one release.
  2. For a verified listing, make your own signing key and sign the manifest. The signature covers every field, the version included, so sign again for every release:
go run github.com/nx-panel/addon-kit/cmd/nx-addon keygen -out addon.key          # keep addon.key out of the repository
go run github.com/nx-panel/addon-kit/cmd/nx-addon sign -key addon.key nx-addon.json > signed.json && mv signed.json nx-addon.json
go run github.com/nx-panel/addon-kit/cmd/nx-addon check nx-addon.json
go run github.com/nx-panel/addon-kit/cmd/nx-addon verify -pub <your public key> nx-addon.json
  1. Submit the repository on the submission form, with your public key for a verified review. The form reads the repository on the spot and says what is wrong, if anything.
  2. NX reviews it. Once listed, the directory reads the latest release again every ten minutes; panels read the signed index daily, or when the operator asks.

A listing whose latest release stops passing the checks keeps showing its last good version, marked as stale, until a release passes again.

Updates

Publish a release with a higher version: the directory picks it up within ten minutes, and each panel puts a badge on your addon at its next read. The operator updates by command or, for an official or verified addon, from the panel; the install script runs again in place.

An update that asks for new scopes or events is installed, but the panel holds the new permissions until the operator approves them; the addon keeps working with the old ones meanwhile. Ask for what you need in the release that needs it.