- TypeScript 99.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| assets | ||
| startos | ||
| .dockerignore | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| icon.svg | ||
| instructions.md | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| UPDATING.md | ||
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.mdfor 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.
- Upstream repo: https://github.com/binwiederhier/ntfy
- Wrapper repo: https://github.com/Start9Labs/ntfy-startos
Table of Contents
- Image and Container Runtime
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- Limitations and Differences
- Quick Reference for AI Consumers
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:
primaryUrlinstore.json, copied intobase-urlinsettings.yamlon 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-allclosed. - 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.yamlwith the VAPID keypair,auth.dbwith every user, token, and grant, retained messages, browser push subscriptions, uploaded attachments, and the admin token instore.json. - Restore: complete, and no admin setup task is raised — the admin account and its token come back with everything else.
base-urlfollows 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
- The server denies everything by default. Upstream ships
read-writefor unauthenticated visitors; here nothing works until a grant exists. - The admin password is shown once. Reset User Password is the only recovery.
base-urlis restricted to this interface's HTTPS addresses.- Login and metrics are enabled, both off upstream.
- 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.
- Most management actions require the service to be running, even though they work on the database rather than the API.
- 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