StartOS wrapper for the NTFY.sh notification service
  • TypeScript 99.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Matt Hill 499388eb2c
Merge pull request #19 from Start9Labs/next
chore: bump ntfy to 2.32.0; 2.31.1:0 → 2.32.0:0
2026-10-18 11:42:05 -06:00
.github/workflows chore: move to start-sdk 3.0.2; 2.28.0:0 → 2.28.0:1 2026-10-05 22:41:57 -06:00
assets chore: adopt syncNext workflow and .gitkeep convention 2026-08-16 23:01:25 -06:00
startos chore: bump ntfy to 2.32.0; 2.31.1:0 → 2.32.0:0 2026-10-18 15:05:15 +00:00
.dockerignore Initial commit 2026-03-20 19:56:36 -04:00
.gitignore Initial commit 2026-03-20 19:56:36 -04:00
.prettierrc chore: move to start-sdk 3.0.2; 2.28.0:0 → 2.28.0:1 2026-10-05 22:41:57 -06:00
AGENTS.md chore: move to start-sdk 3.0.2; 2.28.0:0 → 2.28.0:1 2026-10-05 22:41:57 -06:00
CLAUDE.md docs: standardise package doc set and expand UPDATING.md 2026-05-19 19:07:17 -06:00
icon.svg Release 2.0.0:0 — action restructure, consolidated configure, SDK 1.3.2 2026-04-21 18:29:40 -06:00
instructions.md fix: move Base URL to Set Base URL; show the pkg_{id} and {username}_* placeholders 2026-10-06 14:55:34 -06:00
LICENSE fix: correct LICENSE to match upstream (Apache-2.0); bump start-sdk to 1.5.3; sync s9pk.mk with hello-world 2026-05-28 17:00:24 -06:00
Makefile chore: migrate to start-sdk 2.0 and bump ntfy to 2.26.3 (#8) 2026-07-20 20:18:31 -06:00
package-lock.json chore: bump ntfy to 2.30.0; chore: bump start-sdk to 3.0.6; 2.29.0:0 → 2.30.0:0 2026-10-12 15:13:25 +00:00
package.json chore: bump ntfy to 2.30.0; chore: bump start-sdk to 3.0.6; 2.29.0:0 → 2.30.0:0 2026-10-12 15:13:25 +00:00
README.md docs: re-provisioning a publisher keeps its earlier grants and tokens 2026-10-06 22:59:06 -06:00
tsconfig.json chore: migrate to start-sdk 2.0 and bump ntfy to 2.26.3 (#8) 2026-07-20 20:18:31 -06:00
UPDATING.md chore: bump ntfy to 2.32.0; 2.31.1:0 → 2.32.0:0 2026-10-18 15:05:15 +00:00

NTFY Logo

NTFY on StartOS

Everything not listed in this document should behave the same as upstream ntfy. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable — see the Documentation section of instructions.md for links.

ntfy is a pub-sub notification service: publish to a topic over HTTP, subscribe from a phone, a browser, or a script. This package closes the server by default — nothing is readable or writable without a grant — and drives ntfy's user, token, and access management through actions instead of a shell.


Table of Contents


Image and Container Runtime

One upstream image, unmodified.

Property Value
Image binwiederhier/ntfy
Architectures x86_64, aarch64
Command ntfy serve --config /data/settings.yaml
Subcontainer Purpose
ntfy-main-sub The primary daemon — the one to attach to
ntfy-*-sub Temporary; one per action, each running ntfy's own CLI against the auth database

Every user, token, and access-control action runs ntfy in a short-lived container against the same /data mount rather than talking to the running server, so most of them work whether or not the daemon is healthy. The monitoring actions are the exception — they query the live server's API.

Volume and Data Layout

Two volumes, and one of them never enters a container.

Volume Mount Point Purpose
main /data settings.yaml, the auth, cache, and web-push databases, and uploaded attachments
startos — (host side) store.json; never mounted into a container

Everything ntfy persists lives under /data: auth.db (users, tokens, grants), cache.db (retained messages), webpush.db (browser subscriptions), and attachments/.

File Models

Two models: ntfy's own configuration, and a small store for the one credential the package keeps.

File Volume Format Modelled Written by
settings.yaml main YAML Yes — FileHelper.yaml Install, every init, and the Configure action
store.json startos JSON Yes — FileHelper.json The Set Admin Password and Set Base URL actions

Within settings.yaml:

Enforced — rewritten whenever the package writes: the listen address, the three database paths, the attachment directory, and the four settings below.

Seeded once at install — the VAPID keypair for web push, generated by ntfy webpush keys.

Kept in step with Set Base URL — base-url, rewritten on every init from the URL chosen in Set Base URL (see Network Access and Interfaces). ntfy refuses to start without it, because web push and attachments are configured.

Yours — everything the Configure action exposes: self-registration, the three attachment limits, the per-user daily bandwidth budget, message retention, the VAPID contact email, and the log level. Leaving a field blank removes the key, which hands the setting back to ntfy's own default.

Anything else — the shape is a z.looseObject, so a key the package does not model survives every write. A setting ntfy supports but Configure does not can be added to the file by hand.

Six settings depart from upstream's defaults:

Key Here Upstream Why
auth-default-access deny-all read-write A personal server should not be a world-writable relay on first boot
enable-login true false Accounts are the whole access model here, so the login page has to exist
behind-proxy true false StartOS terminates TLS in front of ntfy; without this every client looks like one IP to the rate limiter
enable-metrics true false Backs the Server Metrics action
attachment-expiry-duration 12h 3h Matches ntfy's own message-retention default, so an attachment cannot expire before the message linking to it
web-push-email-address a placeholder none ntfy refuses to start on a partial web-push configuration; Configure is where you replace it

store.json holds adminToken — a never-expiring admin token minted at setup so the package's monitoring actions can authenticate against ntfy's admin API without keeping the password. Tokens survive password changes, so rotating the admin password does not invalidate it. It also holds primaryUrl, the URL chosen in Set Base URL.

Dependencies

None. Other packages relate to ntfy in the opposite direction — see Provision Publisher.

Network Access and Interfaces

One interface, serving the web app, the subscribe/publish API, and the metrics endpoint.

Interface Id Type Port Description
Web UI ui ui 80 The web interface of NTFY

The port is bound on the ui-multi MultiHost and is not masked.

base-url is one of this interface's HTTPS addresses, chosen with Set Base URL (sdk.setupPrimaryUrl with ssl: true). ntfy builds attachment download links from it, and labels each web push with <base-url>/<topic>; the web app's service worker shows a push only if that matches a subscription it made from its own origin. A browser subscribes over HTTPS, so a plain-HTTP base-url would never match. While the chosen address is gone, base-url follows the preferred HTTPS address (a public domain, then .local) and an important task asks for a new choice; the choice itself is kept, and is used again when it returns. Open UI opens the same URL (preferredLauncherAddress).

Installation and First-Run Flow

Install does three things before you see anything: it generates the VAPID keypair web push needs, it writes the preferred HTTPS address as base-url, and it raises a critical task to create the admin account. The Set Base URL task is raised too, important, pre-filled with that address.

Running that task creates the admin user with a generated password, shows it once, and mints the management token. Save the password when it is offered — the action does not show it again, and Reset User Password is the way back.

The server denies everything until you grant it. A fresh install has one admin and no topic access for anyone else, which is the opposite of upstream's default. Publishing or subscribing from anything means either creating a user and granting them a topic, provisioning a publisher, or opening a topic to anonymous access.

Actions

Twelve actions in five groups, plus the hidden setup action.

Set Admin Password (hidden)

Not user-facing — the install task is what surfaces it. It creates the admin account, shows the generated password once, and mints the never-expiring management token into store.json.

  • Repeat safety: it is a one-time setup action. To rotate the admin password later, use Reset User Password and pick admin.

Configure

Every general server setting: self-registration, attachment limits, the per-user daily bandwidth budget, message retention, VAPID contact email, and log level.

  • What it changes: the corresponding keys in settings.yaml.
  • Cost: seconds, then a restart.
  • Repeat safety: idempotent; the form is pre-filled from the current file.
  • Blank means "upstream default", not zero — clearing a field removes the key rather than pinning it.
  • The limits are checked against each other: a per-file cap above the server-wide total, or a per-user quota above it, is rejected rather than written.

Set Base URL

Picks the URL ntfy uses for attachment links and web push, from this interface's HTTPS addresses. The form pre-selects a public domain if there is one, else the .local address.

  • What it changes: primaryUrl in store.json, copied into base-url in settings.yaml on the next init.
  • Cost: seconds, then a restart.
  • Web push follows it: browsers that subscribed from another address stop receiving pushes until they subscribe again from this one.

Users — Create User, Reset User Password, Delete User, Grant User Topic Access

All four are only available while the service is running and all four operate on auth.db directly.

  • Create User adds an account and shows a generated password once. The new user can do nothing until granted a topic.
  • Reset User Password generates a new password for any account including admin. The old one stops working; tokens do not.
  • Delete User removes an account with its grants and tokens. Admins cannot be deleted.
  • Grant User Topic Access sets one permission — read-write, read-only, write-only, or deny — on a topic or a wildcard pattern, replacing any existing grant for that pair. It can scope a user to their own <username>_* namespace in one step, and shows the user's full grant list afterwards.

Publishers — Provision Publisher, Revoke Publisher

A pair for automation, and both are callable by dependent packages rather than only by a person.

  • Provision Publisher mints a pkg_<id> account with write-only access to one topic and returns a never-expiring token, along with the bridge URL to publish to. It exists so credentials handed to a script or another service carry no more authority than publishing to that one topic. Any regular user can publish too; this is for when you want the narrower grant.
  • Revoke Publisher deletes such an account — grants and tokens cascade. Whatever was using those credentials stops publishing until it re-provisions.
  • A dependent acts only on its own account. Run by another service, Provision rejects any Publisher ID but that service's package id, and Revoke any publisher but its pkg_<id>. Run by you, both take any id.
  • Repeat safety: provisioning the same package id again re-mints; revoking one that is gone is an error, not a silent success.

Public Access — Set Anonymous Topic Access

Grants or denies unauthenticated access to a topic, which is how a topic becomes publicly readable or publicly writable.

  • Cost: seconds, and it is the deliberate way to reopen what auth-default-access: deny-all closed.
  • Repeat safety: idempotent per topic; the full list of anonymously-accessible topics is shown afterwards.

UnifiedPush has no dedicated action: instructions.md walks the user through a user with read-only on up* plus anonymous write-only on up*, the grants ntfy's UnifiedPush setup expects.

Monitoring — Server Stats, Server Metrics

Read-only, only while running, and the two that go through the live server's API using the stored admin token — so both fail with a clear message if Set Admin Password was never run. Server Metrics returns the Prometheus endpoint's output: throughput, topic and subscriber counts, attachment storage, and delivery stats.

Tasks

Two tasks.

Task Severity Raised when Cleared when
Set Admin Password critical At install The action runs
Set Base URL important No URL chosen, or the chosen one is no longer an address A current address is chosen, or the chosen one returns

critical because until it runs there is no account at all, and with deny-all in force nothing can publish or subscribe — the server is running but unusable.

Health Checks

One check, on the only daemon.

Check Displayed Method
primary "Web Interface" GET /v1/health on the local port

This is ntfy's own health endpoint rather than a port probe, so it reports the server's readiness and not merely that something is listening. A failure means ntfy did not come up — most often a settings.yaml value it rejects, which it names in the service logs. A partially-configured web push section is the classic one: ntfy refuses to start rather than running without it.

Backups and Restore

Both volumes are copied wholesale — sdk.Backups.ofVolumes('main', 'startos'). No dump step and nothing excluded.

  • Included: settings.yaml with the VAPID keypair, auth.db with every user, token, and grant, retained messages, browser push subscriptions, uploaded attachments, and the admin token in store.json.
  • Restore: complete, and no admin setup task is raised — the admin account and its token come back with everything else.
  • base-url follows the restored server's addresses. If the chosen URL is not one of them, ntfy uses the preferred HTTPS address and the Set Base URL task asks for a new choice.

Limitations and Differences

  1. The server denies everything by default. Upstream ships read-write for unauthenticated visitors; here nothing works until a grant exists.
  2. The admin password is shown once. Reset User Password is the only recovery.
  3. base-url is restricted to this interface's HTTPS addresses.
  4. Login and metrics are enabled, both off upstream.
  5. A placeholder VAPID contact email is written at install so ntfy will start; replace it via Configure if your push provider needs a real one.
  6. Most management actions require the service to be running, even though they work on the database rather than the API.
  7. No riscv64 build. x86_64 and aarch64 only.

Quick Reference for AI Consumers

package_id: ntfy
image: binwiederhier/ntfy
architectures:
  - x86_64
  - aarch64
subcontainers:
  - ntfy-main-sub # the running daemon
  - ntfy-init-sub # temporary; install-time VAPID key generation
  - ntfy-set-admin-password-sub # temporary; one such sub per action
volumes:
  main: /data
  startos: host side (store.json)
file_models:
  - /data/settings.yaml
  - store.json
startos_managed_env_vars:
  - NTFY_AUTH_FILE # action subcontainers only
  - NTFY_PASSWORD # action subcontainers only
dependencies: []
interfaces:
  ui: { type: ui, port: 80 }
actions:
  - set-admin-password # hidden; surfaced by the install task
  - configure
  - set-base-url
  - create-user # only-running
  - reset-user-password # only-running
  - delete-user # only-running
  - grant-user-topic-access # only-running
  - provision-publisher # only-running; access: dependent
  - revoke-publisher # only-running; access: dependent
  - set-anonymous-topic-access # only-running
  - server-stats # only-running
  - server-metrics # only-running
tasks:
  - { action: set-admin-password, severity: critical }
  - { action: set-base-url, severity: important }
health_checks:
  - primary # displayed "Web Interface"; GET /v1/health