Merge remote-tracking branch 'origin/main' into up-payer

# Conflicts:
#	docs/releases.md
This commit is contained in:
binwiederhier committed 2026-11-01 11:13:01 +00:00
commit b65e642d05
16 files changed
+149 -97

No files matched your search

-4
View File
@@ -15,9 +15,5 @@ LABEL org.opencontainers.image.version="$VERSION"
RUN apk add --no-cache tzdata
COPY ntfy /usr/bin
# Admin socket directory, writable by any uid (sticky, like /tmp), so the admin socket also works
# when the container runs as a non-root user (-u UID:GID); the socket itself is created with mode 0660
RUN mkdir -p /run/ntfy && chmod 1777 /run/ntfy
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]
-4
View File
@@ -17,9 +17,5 @@ LABEL org.opencontainers.image.version="$VERSION"
COPY ntfy /usr/bin
# Admin socket directory, writable by any uid (sticky, like /tmp), so the admin socket also works
# when the container runs as a non-root user (-u UID:GID); the socket itself is created with mode 0660
RUN mkdir -p /run/ntfy && chmod 1777 /run/ntfy
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]
-4
View File
@@ -76,9 +76,5 @@ LABEL org.opencontainers.image.version="$VERSION"
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
# Admin socket directory, writable by any uid (sticky, like /tmp), so the admin socket also works
# when the container runs as a non-root user (-u UID:GID); the socket itself is created with mode 0660
RUN mkdir -p /run/ntfy && chmod 1777 /run/ntfy
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]
+2 -2
View File
@@ -39,7 +39,7 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-admin-unix", Aliases: []string{"listen_admin_unix"}, EnvVars: []string{"NTFY_LISTEN_ADMIN_UNIX"}, Value: server.DefaultListenAdminUnix(), DefaultText: "/run/ntfy/admin.sock (empty on Windows)", Usage: "unix socket path for credential-less local admin access (ntfy user/access/token/tier); \"-\" disables it"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "experimental-listen-admin-unix", Aliases: []string{"experimental_listen_admin_unix"}, EnvVars: []string{"NTFY_EXPERIMENTAL_LISTEN_ADMIN_UNIX"}, Usage: "experimental: unix socket path for credential-less local admin API access, e.g. /run/ntfy/admin.sock; empty or \"-\" disables it"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
@@ -162,7 +162,7 @@ func execServe(c *cli.Context) error {
listenHTTPS := c.String("listen-https")
listenUnix := c.String("listen-unix")
listenUnixMode := c.Int("listen-unix-mode")
listenAdminUnix := c.String("listen-admin-unix")
listenAdminUnix := c.String("experimental-listen-admin-unix")
keyFile := c.String("key-file")
certFile := c.String("cert-file")
firebaseKeyFile := c.String("firebase-key-file")
+16 -1
View File
@@ -498,7 +498,7 @@ func TestCLI_Serve_ListenAdminUnixDisabled(t *testing.T) {
configFile := newEmptyFile(t)
go func() {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "serve", "--config=" + configFile, "--listen-http=-", "--listen-unix=" + sockFile, "--auth-file=" + filepath.Join(dir, "user.db"), "--listen-admin-unix=-"})
err := app.Run([]string{"ntfy", "serve", "--config=" + configFile, "--listen-http=-", "--listen-unix=" + sockFile, "--auth-file=" + filepath.Join(dir, "user.db"), "--experimental-listen-admin-unix=-"})
require.Nil(t, err)
}()
for i := 0; i < 40 && !util.FileExists(sockFile); i++ {
@@ -508,6 +508,21 @@ func TestCLI_Serve_ListenAdminUnixDisabled(t *testing.T) {
require.False(t, util.FileExists(filepath.Join(dir, "-")))
}
func TestCLI_Serve_ExperimentalListenAdminUnix(t *testing.T) {
dir := t.TempDir()
adminSockFile := filepath.Join(dir, "admin.sock")
configFile := newEmptyFile(t)
go func() {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "serve", "--config=" + configFile, "--listen-http=-", "--listen-unix=" + filepath.Join(dir, "ntfy.sock"), "--auth-file=" + filepath.Join(dir, "user.db"), "--experimental-listen-admin-unix=" + adminSockFile})
require.Nil(t, err)
}()
for i := 0; i < 40 && !util.FileExists(adminSockFile); i++ {
time.Sleep(50 * time.Millisecond)
}
require.True(t, util.FileExists(adminSockFile))
}
func TestCLI_Serve_WebSocket(t *testing.T) {
port := 10000 + rand.Intn(20000)
go func() {
+12 -17
View File
@@ -919,27 +919,22 @@ defines access tokens for these users. `phil` has a token `tk_3gd7d2yftt4b8ixyfe
has a token `tk_f099we8uzj7xi5qshzajwp6jffvkz` with the label "Backup script".
### Admin socket
The `ntfy user`, `ntfy access`, `ntfy token` and `ntfy tier` commands talk to the running ntfy server through a local
admin socket, by default `/run/ntfy/admin.sock` (`/var/run/ntfy/admin.sock` on macOS). That way, changes take effect
immediately: a removed user is disconnected right away, a new tier applies to the user's next request, and there is no
delay until the server reloads its access control list.
!!! warning
The admin socket is **experimental**. Options and behavior may still change between releases. The `ntfy user`,
`ntfy access`, `ntfy token` and `ntfy tier` commands do not use it yet; they still write to the database directly.
ntfy can serve its admin API on a local unix socket, set with `experimental-listen-admin-unix` (e.g.
`/run/ntfy/admin.sock`). It is off by default.
The socket needs no password or token: **being able to open the socket is the authorization**. It is created with
mode `0660` in a directory with mode `0750`, both owned by the user ntfy runs as, so only that user, members of its group
and `root` can use it. With the Debian/Ubuntu/Fedora packages, the shipped systemd unit creates `/run/ntfy` (via
`RuntimeDirectory=ntfy`), so `sudo ntfy user add ...` and members of the `ntfy` group just work.
`RuntimeDirectory=ntfy`), so a socket there just works.
The socket is only opened if [access control](#access-control) is configured (`auth-file` or `database-url`). If the
server cannot create the socket, it logs a warning at startup and runs without it:
```
WARN Admin socket /run/ntfy/admin.sock not available, 'ntfy user/access/token/tier' will write to the database directly, which is deprecated, see https://ntfy.sh/docs/deprecations/ (error=mkdir /run/ntfy: permission denied, tag=startup)
```
To change the path, set `listen-admin-unix` in the `server.yml` file; the commands read it from there, too. To disable the
socket, set `listen-admin-unix: "-"`. The admin socket is not available on Windows. If you change the path, pick a
directory that only the ntfy user can write to; otherwise the server logs a warning at startup, since other users may be
able to use or replace the socket.
server cannot create the socket, it logs a warning at startup and runs without it. The admin socket is not available on
Windows. Pick a directory that only the ntfy user can write to; otherwise the server logs a warning at startup, since
other users may be able to use or replace the socket.
### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`,
@@ -2554,7 +2549,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on |
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
| `listen-admin-unix` | `NTFY_LISTEN_ADMIN_UNIX` | *filename* | `/run/ntfy/admin.sock` | Path of the local [admin socket](#admin-socket) the `ntfy user/access/token/tier` commands use; `"-"` disables it. Only opened if auth is configured; not available on Windows |
| `experimental-listen-admin-unix` | `NTFY_EXPERIMENTAL_LISTEN_ADMIN_UNIX` | *filename* | - | Experimental. Path of the local [admin socket](#admin-socket), e.g. `/run/ntfy/admin.sock`; off if empty or `"-"`. Only opened if auth is configured; not available on Windows |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). |
@@ -2677,7 +2672,7 @@ OPTIONS:
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
--listen-admin-unix value, --listen_admin_unix value unix socket path for credential-less local admin access (ntfy user/access/token/tier); "-" disables it (default: /run/ntfy/admin.sock (empty on Windows)) [$NTFY_LISTEN_ADMIN_UNIX]
--experimental-listen-admin-unix value, --experimental_listen_admin_unix value experimental: unix socket path for credential-less local admin API access, e.g. /run/ntfy/admin.sock; empty or "-" disables it [$NTFY_EXPERIMENTAL_LISTEN_ADMIN_UNIX]
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
--firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
+38 -38
View File
@@ -34,37 +34,37 @@ as a service starting at boot time.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_amd64.tar.gz
tar zxvf ntfy_2.35.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.35.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.35.0_linux_amd64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_amd64.tar.gz
tar zxvf ntfy_2.36.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.36.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.36.0_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv6.tar.gz
tar zxvf ntfy_2.35.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.35.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.35.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv6.tar.gz
tar zxvf ntfy_2.36.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.36.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.36.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv7.tar.gz
tar zxvf ntfy_2.35.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.35.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.35.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv7.tar.gz
tar zxvf ntfy_2.36.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.36.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.36.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_arm64.tar.gz
tar zxvf ntfy_2.35.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.35.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.35.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_arm64.tar.gz
tar zxvf ntfy_2.36.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.36.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.36.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -84,25 +84,25 @@ Install the ntfy server unit file (which contains parameters to start the servic
=== "x86_64/amd64"
```bash
sudo mv ntfy_2.35.0_linux_amd64/server/ntfy.service /etc/systemd/system/
sudo mv ntfy_2.36.0_linux_amd64/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "armv6"
```bash
sudo mv ntfy_2.35.0_linux_armv6/server/ntfy.service /etc/systemd/system/
sudo mv ntfy_2.36.0_linux_armv6/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "armv7/armhf"
```bash
sudo mv ntfy_2.35.0_linux_armv7/server/ntfy.service /etc/systemd/system/
sudo mv ntfy_2.36.0_linux_armv7/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "arm64"
```bash
sudo mv ntfy_2.35.0_linux_arm64/server/ntfy.service /etc/systemd/system/
sudo mv ntfy_2.36.0_linux_arm64/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
@@ -118,25 +118,25 @@ Install the ntfy server service script:
=== "x86_64/amd64"
```bash
sudo mv ntfy_2.35.0_linux_amd64/server/ntfy.openrc /etc/init.d/ntfy
sudo mv ntfy_2.36.0_linux_amd64/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "armv6"
```bash
sudo mv ntfy_2.35.0_linux_armv6/server/ntfy.openrc /etc/init.d/ntfy
sudo mv ntfy_2.36.0_linux_armv6/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "armv7/armhf"
```bash
sudo mv ntfy_2.35.0_linux_armv7/server/ntfy.openrc /etc/init.d/ntfy
sudo mv ntfy_2.36.0_linux_armv7/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "arm64"
```bash
sudo mv ntfy_2.35.0_linux_arm64/server/ntfy.openrc /etc/init.d/ntfy
sudo mv ntfy_2.36.0_linux_arm64/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
@@ -204,7 +204,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -212,7 +212,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -220,7 +220,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -228,7 +228,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -238,28 +238,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -301,18 +301,18 @@ pkg install go-ntfy
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_darwin_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_darwin_all.tar.gz > ntfy_2.35.0_darwin_all.tar.gz
tar zxvf ntfy_2.35.0_darwin_all.tar.gz
sudo cp -a ntfy_2.35.0_darwin_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_darwin_all.tar.gz > ntfy_2.36.0_darwin_all.tar.gz
tar zxvf ntfy_2.36.0_darwin_all.tar.gz
sudo cp -a ntfy_2.36.0_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.35.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.36.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -333,7 +333,7 @@ brew install ntfy
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
To install, you can either
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.35.0/ntfy_2.35.0_windows_amd64.zip),
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.36.0/ntfy_2.36.0_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
+30 -14
View File
@@ -6,12 +6,38 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
| Component | Version | Release date |
|------------------|---------|---------------|
| ntfy server | v2.35.0 | Oct 30, 2026 |
| ntfy server | v2.36.0 | Oct 31, 2026 |
| ntfy Android app | v1.25.2 | July 23, 2026 |
| ntfy iOS app | v1.7.0 | May 30, 2026 |
Please check out the release notes for [upcoming releases](#not-released-yet) below.
## ntfy server v2.36.0
Released October 31, 2026
This release keeps the daily limits across restarts and, with PostgreSQL, shares them between servers, so a visitor
gets one daily allowance in total. Each visitor's day now starts at its own time instead of midnight UTC. It also adds
an experimental local admin socket.
**Features:**
* Daily limits (messages, e-mails, phone calls) are stored in the user database, so they survive restarts; with PostgreSQL, all servers share them. Each visitor's day starts at its own time instead of midnight UTC, and the account page and `/v1/account` (`stats.reset_at`) show when it resets ([#2131](https://github.com/binwiederhier/ntfy/pull/2131), see [message limits](config.md#message-limits))
* Experimental local admin socket (`experimental-listen-admin-unix`, off by default) that serves the admin API without credentials to whoever can open it; the shipped systemd unit sets `RuntimeDirectory=ntfy` for it (see [admin socket](config.md#admin-socket))
* Export `ntfy_db_up` per PostgreSQL pool, and health-check the primary like the replicas ([#2133](https://github.com/binwiederhier/ntfy/pull/2133))
**Upgrade notes:**
* Everyone's daily counts start over at the upgrade: the `stats_*` columns of the user table are zeroed and no longer used
* Servers that share a PostgreSQL database must keep their clocks in sync (NTP)
* Visitors without a tier have a daily e-mail limit again (24h / `visitor-email-limit-replenish`)
* The whole server config is checked again when `base-url` or a `message-size-limit` above 4K is set, so an invalid config that used to start now fails at startup ([#1969](https://github.com/binwiederhier/ntfy/pull/1969), thanks to [@mikejgray](https://github.com/mikejgray) for the contribution)
**Bug fixes + maintenance:**
* Web app: Messages replayed by a poll or a reconnect no longer bring back deleted notifications, lose their read state or notify again ([#2098](https://github.com/binwiederhier/ntfy/issues/2098), [#2090](https://github.com/binwiederhier/ntfy/issues/2090), thanks to [@mattford63](https://github.com/mattford63), [@arminus](https://github.com/arminus) and [@tobykaspar79](https://github.com/tobykaspar79) for reporting)
* CLI: `ntfy pub --wait-pid` now waits for processes owned by other users instead of failing with "not running" ([#1710](https://github.com/binwiederhier/ntfy/pull/1710), thanks to [@bekoeppel](https://github.com/bekoeppel) for the contribution)
* Admin API (internal): manage other users' tokens, tiers and password reset links, return 4xx instead of 500 for invalid input, and remove a deleted user's web push subscriptions
## ntfy server v2.35.0
Released October 30, 2026
@@ -2288,31 +2314,21 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
### ntfy server v2.36.0 (UNRELEASED)
### ntfy server v2.36.1 (UNRELEASED)
**Features:**
* Daily limits (messages, e-mails, phone calls) are kept in the user database (`auth-file` for SQLite, or `database-url` for PostgreSQL) and survive restarts; with PostgreSQL, all servers share them, so a visitor gets one daily allowance in total, not one per server (see [message limits](config.md#message-limits))
* Local admin socket (`listen-admin-unix`, default `/run/ntfy/admin.sock`) that serves the admin API without credentials to whoever can open it (the ntfy user and group, and root); the shipped systemd unit now sets `RuntimeDirectory=ntfy` so the server can create it. Custom unit files without it log a warning at startup and keep working as before (see [admin socket](config.md#admin-socket))
* Admin API (internal, undocumented): manage tokens of other users, tiers and password reset links
* Export `ntfy_db_up` (1 if the last health check of a PostgreSQL pool's database succeeded, per pool) and health-check the primary like the replicas, to alert on a server that is cut off from its database (see [PostgreSQL](config.md#postgresql))
* With `visitor-subscriber-rate-limiting`, the subscriber of a UnifiedPush topic (who pays for its pushes) is kept in the user database and shared by all servers, so a push to any server is billed to the subscriber instead of answered with 507 (see [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting))
**Behavior changes:**
* Daily limits now reset on a rolling 24-hour period per account or IP address instead of for everyone at midnight UTC: each visitor's period starts at its own time of day, which spreads the resets over the day. The account page and the `/v1/account` API (`stats.reset_at`) show when the visitor's counts reset
* Visitors without a tier have a daily e-mail limit again (24h / `visitor-email-limit-replenish`), now that it is shared between servers and survives restarts
* The daily reset job is gone; the `stats_*` columns of the user table are zeroed by the upgrade and no longer read or written
* With `visitor-subscriber-rate-limiting`, a UnifiedPush push to a topic that has had no subscriber (subscribe, poll or open stream) for 7 days is answered with `404` (error code 40402) instead of `507`, so app servers like Mastodon remove the expired subscription ([#664](https://github.com/binwiederhier/ntfy/issues/664)); a Matrix push to such a topic gets its pushkey rejected (before: after 12 hours without a subscriber on the server that got the push)
* With `visitor-subscriber-rate-limiting`, UnifiedPush messages are counted against the subscriber's account only while it has a tier, and against the subscriber's IP address otherwise (also after the account lost its tier or was deleted); a tier change applies within a minute
**Bug fixes + maintenance:**
* Admin API: return 4xx errors instead of a 500 for invalid input, and remove a deleted user's web push subscriptions
* Web app: Deleted notifications no longer come back when a poll replays messages published in the same second as the last one ([#2098](https://github.com/binwiederhier/ntfy/issues/2098), thanks to [@mattford63](https://github.com/mattford63) and [@arminus](https://github.com/arminus) for reporting)
* Web app: A message replayed after a reconnect keeps its read state and no longer notifies again, and an older version of a sequence no longer replaces a newer one ([#2090](https://github.com/binwiederhier/ntfy/issues/2090), thanks to [@tobykaspar79](https://github.com/tobykaspar79) for reporting)
* Check the whole server config again when `base-url` or a `message-size-limit` above 4K is set; most checks after them were skipped, so an invalid config (e.g. `upstream-base-url` equal to `base-url`) now fails to start ([#1969](https://github.com/binwiederhier/ntfy/pull/1969), thanks to [@mikejgray](https://github.com/mikejgray) for the contribution)
* CLI: `ntfy pub --wait-pid` now waits for processes owned by other users instead of failing with "not running" ([#1710](https://github.com/binwiederhier/ntfy/pull/1710), thanks to [@bekoeppel](https://github.com/bekoeppel) for the contribution)
* With PostgreSQL and several servers, daily usage a server counted in the last seconds of a visitor's 24-hour period is no longer added to the next period (a double count); it is dropped instead
* Cluster: a forwarded message for a topic without subscribers on the receiving node now counts as wasted (`ntfy_cluster_messages_wasted_total`) and no longer keeps that idle topic in memory
### ntfy iOS app v1.8.0 (UNRELEASED)
+1 -1
View File
@@ -272,7 +272,7 @@ func NewConfig() *Config {
ListenHTTPS: "",
ListenUnix: "",
ListenUnixMode: 0,
ListenAdminUnix: DefaultListenAdminUnix(),
ListenAdminUnix: "", // Experimental, off unless configured
KeyFile: "",
CertFile: "",
DatabaseURL: "",
+1
View File
@@ -11,6 +11,7 @@ func TestConfig_New(t *testing.T) {
c := server.NewConfig()
assert.Equal(t, []string{server.DefaultListenHTTP}, c.ListenHTTP)
assert.Equal(t, server.DefaultKeepaliveInterval, c.KeepaliveInterval)
assert.Equal(t, "", c.ListenAdminUnix) // Experimental, off unless configured
}
func TestConfig_HashExcludesSecrets(t *testing.T) {
+2 -2
View File
@@ -563,8 +563,8 @@ func (s *Server) Run() error {
s.adminUnixListener, err = s.listenAdminSocket()
if err != nil {
s.mu.Unlock()
// Not fatal, unlike the other listeners: the CLI falls back to writing to the database (deprecated)
log.Tag(tagStartup).Err(err).Warn("Admin socket %s not available, 'ntfy user/access/token/tier' will write to the database directly, which is deprecated, see https://ntfy.sh/docs/deprecations/", s.config.ListenAdminUnix)
// Not fatal, unlike the other listeners: nothing depends on the (experimental) admin socket yet
log.Tag(tagStartup).Err(err).Warn("Admin socket %s not available", s.config.ListenAdminUnix)
return
}
defer s.adminUnixListener.Close()
+3 -4
View File
@@ -31,12 +31,11 @@
# listen-unix: <socket-path>
# listen-unix-mode: <linux permissions, e.g. 0700>
# Local admin socket for the "ntfy user", "ntfy access", "ntfy token" and "ntfy tier" commands. Whoever can open
# the socket (mode 0660: the ntfy user and group, and root) can manage users without a password. The commands
# fall back to writing to the database directly if no server listens on it. Set to "-" to disable.
# Experimental: local admin socket that serves the admin API without credentials. Whoever can open the
# socket (mode 0660: the ntfy user and group, and root) can manage users without a password. Off by default.
# The socket is only opened if auth-file or database-url is set. Not available on Windows.
#
# listen-admin-unix: "/run/ntfy/admin.sock"
# experimental-listen-admin-unix: "/run/ntfy/admin.sock"
# Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set.
#
+2 -2
View File
@@ -527,7 +527,7 @@ func (s *Server) handleAdminTiersDelete(w http.ResponseWriter, r *http.Request,
// listenAdminSocket creates the socket's directory, removes a stale socket file (one with no
// server behind it) and listens. Anything at the path that is not a socket is left alone, in case
// listen-admin-unix points somewhere it should not (e.g. at the database). A socket that answers
// experimental-listen-admin-unix points somewhere it should not (e.g. at the database). A socket that answers
// belongs to another running server; in that case this one does not take it over.
func (s *Server) listenAdminSocket() (net.Listener, error) {
path := s.config.ListenAdminUnix
@@ -542,7 +542,7 @@ func (s *Server) listenAdminSocket() (net.Listener, error) {
conn.Close()
return nil, fmt.Errorf("another server is listening on %s", path)
} else if errors.Is(err, fs.ErrPermission) {
return nil, fmt.Errorf("%s belongs to a server of another user, set listen-admin-unix to a path of your own", path)
return nil, fmt.Errorf("%s belongs to a server of another user, set experimental-listen-admin-unix to a path of your own", path)
}
if stat, err := os.Lstat(path); err == nil {
if stat.Mode().Type() != fs.ModeSocket {
+2 -2
View File
@@ -1458,8 +1458,8 @@ func TestAdminSocket_UnwritableDirectory(t *testing.T) {
output := captureLogUntil(t, "not available", func() {
runTestServer(t, s)
})
require.Contains(t, output, "Admin socket")
require.Contains(t, output, "directly, which is deprecated, see https://ntfy.sh/docs/deprecations/")
require.Contains(t, output, "Admin socket "+conf.ListenAdminUnix+" not available")
require.NotContains(t, output, "deprecated") // The CLI does not use the socket yet, so there is nothing to fall back from
require.False(t, adminSocketListening(s))
}
+7 -2
View File
@@ -12,12 +12,17 @@ import (
// node's local subscribers. It is the receive-side counterpart to Cluster.ForwardMessage: local
// delivery and all global side effects (Firebase, email, web push, upstream) already ran on the
// origin node, so this only publishes to the local topic, and never re-relays.
//
// A message whose topic has no subscriber here is counted as wasted and not published: publishing
// would keep the idle topic alive (Keepalive), so a node would never expunge a topic the other
// nodes keep publishing to. A subscriber that comes along later gets it from its since= read of
// the cache, or from the tail (which sends what never went out here).
func (s *Server) deliverFromBus(m *model.Message) {
s.mu.RLock()
t, ok := s.topics[m.Topic]
s.mu.RUnlock()
if !ok {
metrics.ClusterMessagesWasted.Inc() // Relayed here needlessly: this node had no use for the message
if !ok || t.SubscribersCount() == 0 {
metrics.ClusterMessagesWasted.Inc() // Relayed here needlessly: no subscriber here wanted the message
return
}
v := s.visitor(m.Sender, nil)
+33
View File
@@ -11,11 +11,13 @@ import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/cluster"
"heckel.io/ntfy/v2/db"
"heckel.io/ntfy/v2/db/pg"
dbtest "heckel.io/ntfy/v2/db/test"
"heckel.io/ntfy/v2/metrics"
"heckel.io/ntfy/v2/model"
"heckel.io/ntfy/v2/user"
)
@@ -389,6 +391,37 @@ func TestServer_Cluster_DeliverFromBus(t *testing.T) {
require.Empty(t, b.Messages()) // Peer messages are never re-relayed
}
func TestServer_Cluster_DeliverFromBus_WastedWithoutSubscribers(t *testing.T) {
// A fan-out message that no subscriber here wants is counted as wasted: its topic is unknown
// here, or known but without subscribers. The latter is not published either, so the fan-out
// does not keep an idle topic alive (see topicExpungeAfter).
s := newTestServer(t, newTestConfig(t, ""))
s.cluster = &fakeCluster{}
topics, err := s.topicsFromIDs(nil, "idletopic")
require.Nil(t, err)
idle := topics[0]
idle.mu.Lock()
idle.lastAccess = time.Now().Add(-topicExpungeAfter - time.Minute)
idle.mu.Unlock()
wasted := testutil.ToFloat64(metrics.ClusterMessagesWasted)
// Unknown topic
s.deliverFromBus(model.NewDefaultMessage("unknowntopic", "from peer"))
require.Equal(t, wasted+1, testutil.ToFloat64(metrics.ClusterMessagesWasted))
// Known topic without subscribers: wasted, and still stale
s.deliverFromBus(model.NewDefaultMessage("idletopic", "from peer"))
require.Equal(t, wasted+2, testutil.ToFloat64(metrics.ClusterMessagesWasted))
require.True(t, idle.Stale())
// Known topic with a subscriber: delivered, not wasted
idle.Subscribe(func(_ *visitor, m *model.Message) error {
return nil
}, nil, func() {})
s.deliverFromBus(model.NewDefaultMessage("idletopic", "from peer"))
require.Equal(t, wasted+2, testutil.ToFloat64(metrics.ClusterMessagesWasted))
}
func TestServer_Cluster_FirebaseKeepaliverOnlyOnLeader(t *testing.T) {
// Every FCM keepalive wakes all subscribed phones, so only the leader may send them;
// N nodes sending N keepalives would multiply the battery cost for every user