Skip to content

Command-line tools

Helper scripts under deploy/ install and maintain an instance; those under tools/ confirm the operator's callsign and support key management, signing, and verification.

Instance operation — deploy/

cd deploy && ./setup.sh                  # first-run wizard: writes .env (operator call, APP_URL, APRS-IS feed, site call, mail, INGEST_SECRET, OPERATOR_SECRET, FED_PRIVATE_KEY)
deploy/setup.sh --non-interactive --call OE8APR --domain aprs.example.net   # the same from flags (--help lists them)
deploy/aprscaching init <shape>          # set up any shape, then status, doctor, update, backup, restore, rotate-secret, net44 (see below)
deploy/backup.sh                         # SQLite snapshot, uploaded to BACKUP_DIR / OCI_BUCKET / BACKUP_BUCKET + R2_ENDPOINT — run nightly from cron
deploy/cloudflare/cache-rules.sh         # Cloudflare cache rules for a CDN in front of a VM; needs CF_API_TOKEN + CF_ZONE_ID

See Is running an instance for me?, The deploy/aprscaching command and Self-host with Docker.

setup.sh keeps every value already in .env unless you confirm the change (or pass --yes), and never regenerates a secret that is set.

deploy/aprscaching

One command for every shape (The deploy/aprscaching command). Every command takes these options:

Option Effect
--shape SHAPE act on selfhost, ingest-box, baremetal, pocket or desktop instead of the recorded shape
--non-interactive ask nothing; a required value without a default fails and names its flag
--yes confirm every change without asking
--json machine-readable output on stdout (status, doctor, backup); the pocket shape's status has none
--help the command's options; it works on a host with nothing installed yet
Command Options
init selfhost setup.sh's: --call, --passcode, --filter, --domain, --tunnel-token, --lan-host, --site-call, --fed-peers, --fed-submit-instances, --fed-registry-key, --net44-name, --extra-origins, --mail, --email-from, --smtp-host, --smtp-port, --smtp-secure, --smtp-user, --smtp-pass, --resend-key, --app-port, --no-tunnel, --no-next-steps, --env-file, --no-network; and --net44-config FILE, which brings a 44Net Connect tunnel up afterwards (net44 setup)
init baremetal --dir, --user, --repo, --ref, --port, --no-start, --checksum-only, --dry-run, --net44-config, and setup.sh's
init ingest-box --gateway, --code, --shared-secret, --box, --label, --call, --passcode, --filter, --kiss, --meshcom, --site-call, --no-start
init pocket, init desktop Pocket's wizard.sh options; none
status, doctor none
backup --dest, --with-media, --no-settings
restore <archive> --dry-run, --no-settings; the archive may be oci://<bucket>/<object> or oci://<bucket>/latest
update --ref, --rollback-window
rotate-secret <name> --yes; the name is one of INGEST_SECRET, OPERATOR_SECRET, SESSION_SECRET, FED_SUBMIT_SECRET, FED_RELAY_SECRET, FED_CORROBORATION_SECRET
net44 setup <connect.conf> --name, --https (serve the name over https too: One instance, several addresses), --mtu, --no-firewall (44Net)
net44 status, net44 check [name], net44 remove none

Operator callsign — tools/admin/

docker compose exec gateway node tools/admin/verify-call.mjs OE8APR                   # Docker stack, from deploy/
BASE=https://aprs.example.net OPERATOR_SECRET=… node tools/admin/verify-call.mjs OE8APR   # from a checkout

Confirms the operator's own callsign so the sysop role opens on a fresh instance. Run it after signing in as that call — claiming a call clears a verification recorded while nobody held it. It posts to /verify/operator with OPERATOR_SECRET (x-operator-secret; refused while the gateway has none); the gateway accepts only a call listed in ADMIN_CALLSIGNS and marks it verified (method operator). Before it verifies, the script prints the account that holds the call (its id and creation date, the call it operates, its passkey count, whether it has a confirmed email) and asks Verify … for this account? [y/N]; --yes answers for it, and a run without a terminal needs --yes. A call no account holds is not verified. BASE defaults to the gateway on this host, http://127.0.0.1:$PORT (PORT defaults to 8787; the gateway container sets 8080 and carries OPERATOR_SECRET, so the Docker form needs neither). See Who is a sysop.

docker compose exec gateway node tools/admin/signin-link.mjs OE8APR                   # Docker stack, from deploy/
BASE=http://127.0.0.1:8787 OPERATOR_SECRET=… node tools/admin/signin-link.mjs OE8APR     # from a checkout

Prints a one-time sign-in link: the way in on an off-grid instance, where passkeys (no https origin) and email are unavailable. It posts to /auth/operator-link with OPERATOR_SECRET. The link is single-use, expires in 15 minutes, opens a confirm page (opening it signs nobody in), and signs in the account holding the call — or creates one, unverified, for a new call. It never verifies a callsign. On an instance where passkeys or email work, the gateway issues links only for ADMIN_CALLSIGNS calls, unless it runs with OPERATOR_LINKS_FOR_ANY_CALL=1. Run from the box itself, the link names APP_URL.

--link-origin <origin> names another origin for the link: an address in EXTRA_ORIGINS, such as the HAMNET address a member opens (One instance, several addresses), or the station's https hotspot origin (https://<its private IPv4 address>:<HTTPS_PORT>), the one a visitor's phone opens. The gateway refuses any origin other than those and APP_URL. --qr also prints the link as a QR code for the phone to scan (Node 22.18 or later). BASE stays the address the script reaches the gateway on.

OPERATOR_SECRET=… node tools/admin/signin-link.mjs --link-origin https://192.168.43.1:8443 --qr OE8VIS

See Off-grid sign-in.

Mail test — tools/admin/

docker compose exec gateway node tools/admin/mail-test.mjs you@example.net                    # Docker stack, from deploy/
BASE=http://127.0.0.1:8787 OPERATOR_SECRET=… node tools/admin/mail-test.mjs you@example.net     # from a checkout

Sends one test mail over the gateway's own transport (SMTP when SMTP_HOST is set, else the Resend API) by posting to /api/admin/mail-test with OPERATOR_SECRET. It prints the transport on success and the server's reason on a refusal, and exits non-zero then. See Send mail.

Licence registers — tools/licence/

BASE=https://aprs.example.net OPERATOR_SECRET=… node tools/licence/import.mjs --source fcc,ised,acma,at,de
node tools/licence/import.mjs --source all                    # every register
node tools/licence/import.mjs --source de --file liste.pdf    # a file already downloaded (.zip, .pdf or pdftotext .txt)
node tools/licence/import.mjs --source ised --dry-run         # parse and count; send nothing
node tools/licence/import.mjs --list                          # the registers and their ids

Imports public amateur licence registers for the register badge. Each register is downloaded and parsed on this machine; only callsign, status and expiry are posted, in batches of 1000, to /api/licence/import with OPERATOR_SECRET, and the run is closed with /api/licence/import/finish, which removes calls the register no longer lists. With no --source, LICENCE_SOURCES (comma-separated) names the registers, for scheduled runs. BASE (or --base) defaults to http://127.0.0.1:8787. The PDF registers need pdftotext (poppler-utils). Exits non-zero if any register fails. See Licence registers.

Federation keys — tools/fedkey/

node tools/fedkey/genkey.mjs            # generate an instance signing key

Prints FED_PRIVATE_KEY (set it as a secret) and the public key it will publish. Add --raw for machine-readable output. Related:

  • node tools/fedkey/rotatekey.mjs — produce a signed rotation record (a new key vouched for by the old one) for FED_ROTATIONS, so peers accept the new key without interruption, and the old key for FED_KEY_HISTORY with an until of the rotation time plus FED_ROTATION_GRACE_DAYS (default 7): peers stop accepting the old key after that.
  • node tools/fedkey/signregistry.mjs '<entries-json>' — sign an instance-registry document with an authority key, producing FED_REGISTRY + FED_REGISTRY_KEY.

Tool signing — tools/toolkey/

Bundle the project's tool registry into a release of the app:

node tools/toolkey/bundle-registry.mjs v1.0.0                   # bundle a release of the project registry

bundle-registry.mjs copies a tagged release of the project registry into apps/web/public/tools/, after checking every signature and script hash; every instance serves that copy at /tools/registry.json (Tools and plugins). Signing a tool or a registry is on the tools site: Sign a tool and Keys, rotation and release practice.

Development & conformance

The contributor commands — pnpm run check, smoke, verify, conformance:meshcom — the smoke and e2e suites, the CI guards under tools/checks/ and the interop tests are described in Testing & verification.

Note

tools/teaser/ and tools/webauthn/ are internal build/marketing and test helpers, not operator tools.

Next