ntfy Documentation

Send push notifications to your phone or desktop using PUT/POST

33,423 stars Go Go 1 file ยท ~53,008 tokens #curl#notifications#ntfy#ntfysh#pubsub#push-notifications#rest-api
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy ntfy on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name ntfy -p 80:80 --restart always binwiederhier/ntfy
Configured Services & Ports
โš™๏ธ ntfy
image: binwiederhier/ntfy
๐Ÿ”Œ Ports: 80:80
docker-compose.yml
yaml (docker-compose.yml)
services:
  ntfy:
    image: binwiederhier/ntfy
    container_name: ntfy
    command:
      - serve
    environment:
      - TZ=UTC    # optional: Change to your desired timezone
    user: UID:GID # optional: Set custom user/group or uid/gid
    volumes:
      - /var/cache/ntfy:/var/cache/ntfy
      - /etc/ntfy:/etc/ntfy
    ports:
      - 80:80
    restart: unless-stopped

File: README.md


ntfy.sh | Send push notifications to your phone or desktop via PUT/POST










ntfy (pronounced "notify") is a simple HTTP-based pub-sub
notification service. With ntfy, you can send notifications to your phone or desktop via scripts from any computer,
without having to sign up or pay any fees. If you'd like to run your own instance of the service, you can easily do
so since ntfy is open source.

You can access the free version of ntfy at ntfy.sh. There is also an open-source Android app
available on Google Play or F-Droid,
as well as an open source iOS app available on the App Store.



[ntfy Pro](https://ntfy.sh/app) ๐Ÿ’ธ ๐ŸŽ‰

I now offer paid plans for ntfy.sh if you don't want to self-host, or you want to support the development of
ntfy (โ†’ Purchase via web app). You can buy a plan for as low as $5/month.
You can also donate via GitHub Sponsors, and Liberapay.
I would be very humbled by your sponsorship. โค๏ธ

**[Documentation](https://ntfy.sh/docs/)**

Getting started |
Android/iOS |
API |
Install / Self-hosting |
Building

Chat/forum

There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
works best for you:

Announcements/beta testers

For announcements of new releases and cutting-edge beta versions, please subscribe to the ntfy.sh/announcements
topic. If you'd like to test the iOS app, join TestFlight. For Android betas,
join Discord/Matrix (I'll eventually make a testing channel in Google Play).

Sponsors

If you'd like to support the ntfy maintainers, please consider donating to GitHub Sponsors or
and Liberapay. We would be humbled if you helped carry the server and developer
account costs. Even small donations are very much appreciated.

Thank you to our commercial sponsors, who help keep the service running and the development going:

And a big fat Thank You to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:










































































































































Contributing

I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the build instructions
for the server and the Android app. Or, if you'd like to help translate ๐Ÿ‡ฉ๐Ÿ‡ช ๐Ÿ‡บ๐Ÿ‡ธ ๐Ÿ‡ง๐Ÿ‡ฌ, you can start immediately in
Hosted Weblate.

Code of Conduct

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for
everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity
and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,
color, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

Please be sure to read the complete Code of Conduct.

License

Made with โค๏ธ by Philipp C. Heckel.
The project is dual licensed under the Apache License 2.0 and the GPLv2 License.

Third-party libraries and resources:


File: docs/subscribe/api.md

Subscribe via API

You can create and subscribe to a topic in the web UI, via the phone app, via the ntfy CLI,
or in your own app or script by subscribing the API. This page describes how to subscribe via API. You may also want to
check out the page that describes how to publish messages.

You can consume the subscription API as either a simple HTTP stream (JSON, SSE or raw), or
via WebSockets. Both are incredibly simple to use.

HTTP stream

The HTTP stream-based API relies on a simple GET request with a streaming HTTP response, i.e you open a GET request and
the connection stays open forever
, sending messages back as they come in. There are three different API endpoints, which
only differ in the response format:

Subscribe as JSON stream

Here are a few examples of how to consume the JSON endpoint (<topic>/json). For almost all languages, this is the
recommended way to subscribe to a topic
. The notable exception is JavaScript, for which the
SSE/EventSource stream is much easier to work with.

=== "Command line (curl)"
$ curl -s ntfy.sh/disk-alerts/json {"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"} {"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Disk full"} {"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"} ...

=== "ntfy CLI"
$ ntfy subcribe disk-alerts {"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Disk full"} ...

=== "HTTP"
``` http
GET /disk-alerts/json HTTP/1.1
Host: ntfy.sh

text
HTTP/1.1 200 OK
Content-Type: application/x-ndjson; charset=utf-8
Transfer-Encoding: chunked

{"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}
{"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Disk full"}
{"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"}
...
```

=== "Go"
go resp, err := http.Get("https://ntfy.sh/disk-alerts/json") if err != nil { log.Fatal(err) } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { println(scanner.Text()) }

=== "Python"
python resp = requests.get("https://ntfy.sh/disk-alerts/json", stream=True) for line in resp.iter_lines(): if line: print(line)

=== "PHP"
php-inline $fp = fopen('https://ntfy.sh/disk-alerts/json', 'r'); if (!$fp) die('cannot open stream'); while (!feof($fp)) { echo fgets($fp, 2048); flush(); } fclose($fp);

Subscribe as SSE stream

Using EventSource in JavaScript, you can consume
notifications via a Server-Sent Events (SSE) stream. It's incredibly
easy to use. Here's what it looks like. You may also want to check out the full example on GitHub.

=== "Command line (curl)"
```
$ curl -s ntfy.sh/mytopic/sse
event: open
data: {"id":"weSj9RtNkj","time":1635528898,"event":"open","topic":"mytopic"}

text
data: {"id":"p0M5y6gcCY","time":1635528909,"event":"message","topic":"mytopic","message":"Hi!"}

event: keepalive
data: {"id":"VNxNIg5fpt","time":1635528928,"event":"keepalive","topic":"test"}
...
```

=== "HTTP"
``` http
GET /mytopic/sse HTTP/1.1
Host: ntfy.sh

text
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked

event: open
data: {"id":"weSj9RtNkj","time":1635528898,"event":"open","topic":"mytopic"}

data: {"id":"p0M5y6gcCY","time":1635528909,"event":"message","topic":"mytopic","message":"Hi!"}

event: keepalive
data: {"id":"VNxNIg5fpt","time":1635528928,"event":"keepalive","topic":"test"}
...
```

=== "JavaScript"
javascript const eventSource = new EventSource('https://ntfy.sh/mytopic/sse'); eventSource.onmessage = (e) => { console.log(e.data); };

Subscribe as raw stream

The /raw endpoint will output one line per message, and will only include the message body. It's useful for extremely
simple scripts, and doesn't include all the data. Additional fields such as priority,
tags or message title are not included in this output
format. Keepalive messages are sent as empty lines.

=== "Command line (curl)"
```
$ curl -s ntfy.sh/disk-alerts/raw

text
Disk full
...
```

=== "HTTP"
``` http
GET /disk-alerts/raw HTTP/1.1
Host: ntfy.sh

text
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

Disk full
...
```

=== "Go"
go resp, err := http.Get("https://ntfy.sh/disk-alerts/raw") if err != nil { log.Fatal(err) } defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { println(scanner.Text()) }

=== "Python"
python resp = requests.get("https://ntfy.sh/disk-alerts/raw", stream=True) for line in resp.iter_lines(): if line: print(line)

=== "PHP"
php-inline $fp = fopen('https://ntfy.sh/disk-alerts/raw', 'r'); if (!$fp) die('cannot open stream'); while (!feof($fp)) { echo fgets($fp, 2048); flush(); } fclose($fp);

WebSockets

You may also subscribe to topics via WebSockets, which is also widely
supported in many languages. Most notably, WebSockets are natively supported in JavaScript. You may also want to
check out the full example on GitHub.
On the command line, I recommend websocat, a fantastic tool similar to socat
or curl, but specifically for WebSockets.

The WebSockets endpoint is available at <topic>/ws and returns messages as JSON objects similar to the
JSON stream endpoint.

=== "Command line (websocat)"
$ websocat wss://ntfy.sh/mytopic/ws {"id":"qRHUCCvjj8","time":1642307388,"event":"open","topic":"mytopic"} {"id":"eOWoUBJ14x","time":1642307754,"event":"message","topic":"mytopic","message":"hi there"}

=== "HTTP"
``` http
GET /disk-alerts/ws HTTP/1.1
Host: ntfy.sh
Upgrade: websocket
Connection: Upgrade

text
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
...
```

=== "Go"
go import "github.com/gorilla/websocket" ws, _, _ := websocket.DefaultDialer.Dial("wss://ntfy.sh/mytopic/ws", nil) messageType, data, err := ws.ReadMessage() ...

=== "JavaScript"
javascript const socket = new WebSocket('wss://ntfy.sh/mytopic/ws'); socket.addEventListener('message', function (event) { console.log(event.data); });

Advanced features

Poll for messages

You can also just poll for messages if you don't like the long-standing connection using the poll=1
query parameter. The connection will end after all available messages have been read. This parameter can be
combined with since= (defaults to since=all).

text
curl -s "ntfy.sh/mytopic/json?poll=1"

Fetch cached messages

Messages may be cached for a couple of hours (see message caching) to account for network
interruptions of subscribers. If the server has configured message caching, you can read back what you missed by using
the since= query parameter. It takes a duration (e.g. 10m or 30s), a Unix timestamp (e.g. 1635528757),
a message ID (e.g. nFS3knfcQ1xe), or all (all cached messages).

text
curl -s "ntfy.sh/mytopic/json?since=10m"
curl -s "ntfy.sh/mytopic/json?since=1645970742"
curl -s "ntfy.sh/mytopic/json?since=nFS3knfcQ1xe"

Fetch latest message

If you only want the most recent message sent to a topic and do not have a message ID or timestamp to use with
since=, you can use since=latest to grab the most recent message from the cache for a particular topic.

text
curl -s "ntfy.sh/mytopic/json?poll=1&since=latest"

Fetch scheduled messages

Messages that are scheduled to be delivered at a later date are not typically
returned when subscribing via the API, which makes sense, because after all, the messages have technically not been
delivered yet. To also return scheduled messages from the API, you can use the scheduled=1 (alias: sched=1)
parameter (makes most sense with the poll=1 parameter):

text
curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"

Filter messages

You can filter which messages are returned based on the well-known message fields id, message, title, priority and
tags. Here's an example that only returns messages of high or urgent priority that contains the both tags
"zfs-error" and "error". Note that the priority filter is a logical OR and the tags filter is a logical AND.

text
$ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
{"id":"0TIkJpBcxR","time":1640122627,"event":"open","topic":"alerts"}
{"id":"X3Uzz9O1sM","time":1640122674,"event":"message","topic":"alerts","priority":4,
  "tags":["error", "zfs-error"], "message":"ZFS pool corruption detected"}

Available filters (all case-insensitive):

Filter variable Alias Example Description
id X-ID ntfy.sh/mytopic/json?poll=1&id=pbkiz8SD7ZxG Only return messages that match this exact message ID
message X-Message, m ntfy.sh/mytopic/json?message=lalala Only return messages that match this exact message string
title X-Title, t ntfy.sh/mytopic/json?title=some+title Only return messages that match this exact title string
priority X-Priority, prio, p ntfy.sh/mytopic/json?p=high,urgent Only return messages that match any priority listed (comma-separated)
tags X-Tags, tag, ta ntfy.sh/mytopic/json?tags=error,alert Only return messages that match all listed tags (comma-separated)

Subscribe to multiple topics

It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
in the URL. This allows you to reduce the number of connections you have to maintain:

text
$ curl -s ntfy.sh/mytopic1,mytopic2/json
{"id":"0OkXIryH3H","time":1637182619,"event":"open","topic":"mytopic1,mytopic2,mytopic3"}
{"id":"dzJJm7BCWs","time":1637182634,"event":"message","topic":"mytopic1","message":"for topic 1"}
{"id":"Cm02DsxUHb","time":1637182643,"event":"message","topic":"mytopic2","message":"for topic 2"}

Authentication

Depending on whether the server is configured to support access control, some topics
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
To publish/subscribe to protected topics, you can:

  • Use basic auth, e.g. Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
  • or use the auth query parameter, e.g. ?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw

Please refer to the publishing documentation for additional details.

JSON message format

Both the /json endpoint and the /sse endpoint return a JSON
format of the message. It's very straight forward:

Message:

Field Required Type Example Description
id โœ”๏ธ string hwQ2YpKdmg Randomly chosen message identifier
time โœ”๏ธ number 1635528741 Message date time, as Unix time stamp
expires (โœ”)๏ธ number 1673542291 Unix time stamp indicating when the message will be deleted, not set if Cache: no is sent
event โœ”๏ธ open, keepalive, message, message_delete, message_clear, poll_request message Message type, typically you'd be only interested in message
topic โœ”๏ธ string topic1,topic2 Comma-separated list of topics the message is associated with; only one for all message events, but may be a list in open events
sequence_id - string my-sequence-123 Sequence ID for updating/deleting notifications
message - string Some message Message body; always present in message events
title - string Some title Message title; if not set defaults to ntfy.sh/<topic>
tags - string array ["tag1","tag2"] List of tags that may or not map to emojis
priority - 1, 2, 3, 4, or 5 4 Message priority with 1=min, 3=default and 5=max
click - URL https://example.com Website opened when notification is clicked
actions - JSON array see actions buttons Action buttons that can be displayed in the notification
attachment - JSON object see below Details about an attachment (name, URL, size, ...)

Attachment (part of the message, see attachments for details):

Field Required Type Example Description
name โœ”๏ธ string attachment.jpg Name of the attachment, can be overridden with X-Filename, see attachments
url โœ”๏ธ URL https://example.com/file.jpg URL of the attachment
type -๏ธ mime type image/jpeg Mime type of the attachment, only defined if attachment was uploaded to ntfy server
size -๏ธ number 33848 Size of the attachment in bytes, only defined if attachment was uploaded to ntfy server
expires -๏ธ number 1635528741 Attachment expiry date as Unix time stamp, only defined if attachment was uploaded to ntfy server

Here's an example for each message type:

=== "Notification message"
json { "id": "sPs71M8A2T", "time": 1643935928, "expires": 1643936928, "event": "message", "topic": "mytopic", "priority": 5, "tags": [ "warning", "skull" ], "click": "https://homecam.mynet.lan/incident/1234", "attachment": { "name": "camera.jpg", "type": "image/png", "size": 33848, "expires": 1643946728, "url": "https://ntfy.sh/file/sPs71M8A2T.png" }, "title": "Unauthorized access detected", "message": "Movement detected in the yard. You better go check" }

=== "Notification message (minimal)"
json { "id": "wze9zgqK41", "time": 1638542110, "expires": 1638543112, "event": "message", "topic": "phil_alerts", "message": "Remote access to phils-laptop detected. Act right away." }

=== "Open message"
json { "id": "2pgIAaGrQ8", "time": 1638542215, "event": "open", "topic": "phil_alerts" }

=== "Keepalive message"
json { "id": "371sevb0pD", "time": 1638542275, "event": "keepalive", "topic": "phil_alerts" }

=== "Poll request message"
json { "id": "371sevb0pD", "time": 1638542275, "event": "poll_request", "topic": "phil_alerts" }

List of all parameters

The following is a list of all parameters that can be passed when subscribing to a message. Parameter names are case-insensitive,
and can be passed as HTTP headers or query parameters in the URL. They are listed in the table in their canonical form.

Parameter Aliases (case-insensitive) Description
poll X-Poll, po Return cached messages and close connection
since X-Since, si Return cached messages since timestamp, duration or message ID
scheduled X-Scheduled, sched Include scheduled/delayed messages in message list
id X-ID Filter: Only return messages that match this exact message ID
message X-Message, m Filter: Only return messages that match this exact message string
title X-Title, t Filter: Only return messages that match this exact title string
priority X-Priority, prio, p Filter: Only return messages that match any priority listed (comma-separated)
tags X-Tags, tag, ta Filter: Only return messages that match all listed tags (comma-separated)

File: docs/subscribe/cli.md

Subscribe via ntfy CLI

In addition to subscribing via the web UI, the phone app, or the API, you can subscribe
to topics via the ntfy CLI. The CLI is included in the same ntfy binary that can be used to self-host a server.

!!! info
The ntfy CLI is not required to send or receive messages. You can instead send messages with curl,
and even use it to subscribe to topics. It may be a little more convenient to use the ntfy CLI than writing
your own script. It all depends on the use case. ๐Ÿ˜€

Install + configure

To install the ntfy CLI, simply follow the steps outlined on the install page. The ntfy server and
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
by creating ~/.config/ntfy/client.yml (for the non-root user), or /etc/ntfy/client.yml (for the root user). You
can find a skeleton config on GitHub.

If you just want to use ntfy.sh, you don't have to change anything. If you self-host your own server,
you may want to edit the default-host option:

yaml
# Base URL used to expand short topic names in the "ntfy publish" and "ntfy subscribe" commands.
# If you self-host a ntfy server, you'll likely want to change this.
#
default-host: https://ntfy.myhost.com

Publish messages

You can send messages with the ntfy CLI using the ntfy publish command (or any of its aliases pub, send or
trigger). There are a lot of examples on the page about publishing messages, but here are a few
quick ones:

=== "Simple send"
ntfy publish mytopic This is a message ntfy publish mytopic "This is a message" ntfy pub mytopic "This is a message"

=== "Send with title, priority, and tags"
ntfy publish \ --title="Thing sold on eBay" \ --priority=high \ --tags=partying_face \ mytopic \ "Somebody just bought the thing that you sell"

=== "Send at 8:30am"
ntfy pub --at=8:30am delayed_topic Laterzz

=== "Triggering a webhook"
ntfy trigger mywebhook ntfy pub mywebhook

Subscribe to topics

You can subscribe to topics using ntfy subscribe. Depending on how it is called, this command
will either print or execute a command for every arriving message. There are a few different ways
in which the command can be run:

Stream messages as JSON

text
ntfy subscribe TOPIC

If you run the command like this, it prints the JSON representation of every incoming message. This is useful
when you have a command that wants to stream-read incoming JSON messages. Unless --poll is passed, this command
stays open forever.

text
$ ntfy sub mytopic
{"id":"nZ8PjH5oox","time":1639971913,"event":"message","topic":"mytopic","message":"hi there"}
{"id":"sekSLWTujn","time":1639972063,"event":"message","topic":"mytopic",priority:5,"message":"Oh no!"}
...
Subscribe in JSON mode

Run command for every message

text
ntfy subscribe TOPIC COMMAND

If you run it like this, a COMMAND is executed for every incoming messages. Scroll down to see a list of available
environment variables. Here are a few examples:

text
ntfy sub mytopic 'notify-send "$m"'
ntfy sub topic1 /my/script.sh
ntfy sub topic1 'echo "Message $m was received. Its title was $t and it had priority $p'
Execute command on incoming messages

The message fields are passed to the command as environment variables and can be used in scripts. Note that since
these are environment variables, you typically don't have to worry about quoting too much, as long as you enclose them
in double-quotes, you should be fine:

Variable Aliases Description
$NTFY_ID $id Unique message ID
$NTFY_TIME $time Unix timestamp of the message delivery
$NTFY_TOPIC $topic Topic name
$NTFY_MESSAGE $message, $m Message body
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
$NTFY_RAW $raw Raw JSON message

Subscribe to multiple topics

text
ntfy subscribe --from-config

To subscribe to multiple topics at once, and run different commands for each one, you can use ntfy subscribe --from-config,
which will read the subscribe config from the config file. Please also check out the ntfy-client systemd service.

Here's an example config file that subscribes to three different topics, executing a different command for each of them:

=== "~/.config/ntfy/client.yml (Linux)"
yaml subscribe: - topic: echo-this command: 'echo "Message received: $message"' - topic: alerts command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m" if: priority: high,urgent - topic: calc command: 'gnome-calculator 2>/dev/null &' - topic: print-temp command: | echo "You can easily run inline scripts, too." temp="$(sensors | awk '/Pack/ { print substr($4,2,2) }')" if [ $temp -gt 80 ]; then echo "Warning: CPU temperature is $temp. Too high." else echo "CPU temperature is $temp. That's alright." fi

=== "~/Library/Application Support/ntfy/client.yml (macOS)"
yaml subscribe: - topic: echo-this command: 'echo "Message received: $message"' - topic: alerts command: osascript -e "display notification \"$message\"" if: priority: high,urgent - topic: calc command: open -a Calculator

=== "%AppData%\ntfy\client.yml (Windows)"
yaml subscribe: - topic: echo-this command: 'echo Message received: %message%' - topic: alerts command: | notifu /m "%NTFY_MESSAGE%" exit 0 if: priority: high,urgent - topic: calc command: calc

In this example, when ntfy subscribe --from-config is executed:

  • Messages to echo-this simply echos to standard out
  • Messages to alerts display as desktop notification for high priority messages using notify-send (Linux),
    notifu (Windows) or osascript (macOS)
  • Messages to calc open the calculator ๐Ÿ˜€ (because, why not)
  • Messages to print-temp execute an inline script and print the CPU temperature (Linux version only)

I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:

Execute all the things

Using the systemd service

You can use the ntfy-client systemd service (see ntfy-client.service)
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)
if you install the deb/rpm package. To configure it, simply edit /etc/ntfy/client.yml and run sudo systemctl restart ntfy-client.

!!! info
The ntfy-client.service runs as user ntfy, meaning that typical Linux permission restrictions apply. See below
for how to fix this.

If the service runs on your personal desktop machine, you may want to override the service user/group (User= and Group=), and
adjust the DISPLAY and DBUS_SESSION_BUS_ADDRESS environment variables. This will allow you to run commands in your X session
as the primary machine user.

You can either manually override these systemd service entries with sudo systemctl edit ntfy-client, and add this
(assuming your user is phil). Don't forget to run sudo systemctl daemon-reload and sudo systemctl restart ntfy-client
after editing the service file:

=== "/etc/systemd/system/ntfy-client.service.d/override.conf"
[Service] User=phil Group=phil Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
Or you can run the following script that creates this override config for you:

text
sudo sh -c 'cat > /etc/systemd/system/ntfy-client.service.d/override.conf' <<EOF
[Service]
User=$USER
Group=$USER
Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus"
EOF

sudo systemctl daemon-reload
sudo systemctl restart ntfy-client

Authentication

Depending on whether the server is configured to support access control, some topics
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
To publish/subscribe to protected topics, you can use Basic Auth
with a valid username/password. For your self-hosted server, be sure to use HTTPS to avoid eavesdropping and exposing
your password.

You can either add your username and password to the configuration file:
=== "~/.config/ntfy/client.yml"
yaml - topic: secret command: 'notify-send "$m"' user: phill password: mypass

Or with the ntfy subscibe command:

text
ntfy subscribe \
  -u phil:mypass \
  ntfy.example.com/mysecrets

File: docs/subscribe/phone.md

Subscribe from your phone

You can use the ntfy Android App to receive
notifications directly on your phone. Just like the server, this app is also open source.
Since I don't have an iPhone or a Mac, I didn't make an iOS app yet. I'd be awesome if someone else could help out.


You can get the Android app from both Google Play and
from F-Droid. Both are largely identical, with the one exception that
the F-Droid flavor does not use Firebase.

Overview

A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
straight forward. You can add topics and as soon as you add them, you can publish messages to them.

text
[](../../static/img/android-screenshot-main.png)
[](../../static/img/android-screenshot-detail.png)
[](../../static/img/android-screenshot-pause.png)
[](../../static/img/android-screenshot-add.png)
[](../../static/img/android-screenshot-add-instant.png)
[](../../static/img/android-screenshot-add-other.png)

If those screenshots are still not enough, here's a video:

Sending push notifications to your Android phone

Message priority

When you publish messages to a topic, you can define a priority. This priority defines
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.

By default, messages with default priority or higher (>= 3) will vibrate and make a sound. Messages with high or urgent
priority (>= 4) will also show as pop-over, like so:

{ width=500 }
High and urgent notifications show as pop-over

You can change these settings in Android by long-pressing on the app, and tapping "Notifications", or from the "Settings"
menu under "Channel settings". There is one notification channel for each priority:

{ width=500 }
Per-priority channels

Per notification channel, you can configure a channel-specific sound, whether to override the Do Not Disturb (DND)
setting, and other settings such as popover or notification dot:

{ width=500 }
Per-priority sound/vibration settings

Instant delivery

Instant delivery allows you to receive messages on your phone instantly, even when your phone is in doze mode, i.e.
when the screen turns off, and you leave it on the desk for a while. This is achieved with a foreground service, which
you'll see as a permanent notification that looks like this:

{ width=500 }
Instant delivery foreground notification

Android does not allow you to dismiss this notification, unless you turn off the notification channel in the settings.
To do so, long-press on the foreground notification (screenshot above) and navigate to the settings. Then toggle the
"Subscription Service" off:

{ width=500 }
Turning off the persistent instant delivery notification

Limitations without instant delivery: Without instant delivery, messages may arrive with a significant delay
(sometimes many minutes, or even hours later). If you've ever picked up your phone and
suddenly had 10 messages that were sent long before you know what I'm talking about.

The reason for this is Firebase Cloud Messaging (FCM). FCM is the
only Google approved way to send push messages to Android devices, and it's what pretty much all apps use to deliver push
notifications. Firebase is overall pretty bad at delivering messages in time, but on Android, most apps are stuck with it.

The ntfy Android app uses Firebase only for the main host ntfy.sh, and only in the Google Play flavor of the app.
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.

Share to topic

You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
you shared content to and lists them at the bottom.

The feature is pretty self-explanatory, and one picture says more than a thousand words. So here are two pictures:

text
[](../../static/img/android-screenshot-share-1.jpg)
[](../../static/img/android-screenshot-share-2.jpg)

The ntfy Android app supports deep linking directly to topics. This is useful when integrating with automation apps
such as MacroDroid or Tasker,
or to simply directly link to a topic from a mobile website.

!!! info
Android deep linking of http/https links is very brittle and limited, which is why something like https://<host>/<topic>/subscribe is
not possible, and instead ntfy:// links have to be used. More details in issue #20.

Supported link formats:

Link format Example Description
ntfy://<host>/<topic> ntfy://ntfy.sh/mytopic Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view https://ntfy.sh/mytopic (HTTPS!)
ntfy://<host>/<topic>?secure=false ntfy://example.com/mytopic?secure=false Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view http://example.com/mytopic (HTTP!)

Integrations

UnifiedPush

UnifiedPush is a standard for receiving push notifications without using the Google-owned
Firebase Cloud Messaging (FCM) service. It puts push notifications
in the control of the user. ntfy can act as a UnifiedPush distributor, forwarding messages to apps that support it.

To use ntfy as a distributor, simply select it in one of the supported apps.
That's it. It's a one-step installation ๐Ÿ˜€. If desired, you can select your own selfhosted ntfy server
to handle messages. Here's an example with FluffyChat:

text
[](../../static/img/android-screenshot-unifiedpush-fluffychat.jpg)
[](../../static/img/android-screenshot-unifiedpush-subscription.jpg)
[](../../static/img/android-screenshot-unifiedpush-settings.jpg)

Automation apps

The ntfy Android app integrates nicely with automation apps such as MacroDroid
or Tasker. Using Android intents, you can
react to incoming messages, as well as send messages.

React to incoming messages

To react on incoming notifications, you have to register to intents with the io.heckel.ntfy.MESSAGE_RECEIVED action (see
code for details).
Here's an example using MacroDroid
and Tasker, but any app that can catch
broadcasts is supported:

text
[](../../static/img/android-screenshot-macrodroid-overview.png)
[](../../static/img/android-screenshot-macrodroid-trigger.png)
[](../../static/img/android-screenshot-macrodroid-action.png)
[](../../static/img/android-screenshot-tasker-profiles.png)
[](../../static/img/android-screenshot-tasker-event-edit.png)
[](../../static/img/android-screenshot-tasker-task-edit.png)
[](../../static/img/android-screenshot-tasker-action-edit.png)

For MacroDroid, be sure to type in the package name io.heckel.ntfy, otherwise intents may be silently swallowed.
If you're using topics to drive automation, you'll likely want to mute the topic in the ntfy app. This will prevent
notification popups:

{ width=500 }
Muting notifications to prevent popups

Here's a list of extras you can access. Most likely, you'll want to filter for topic and react on message:

Extra name Type Example Description
id String bP8dMjO8ig Randomly chosen message identifier (likely not very useful for task automation)
base_url String https://ntfy.sh Root URL of the ntfy server this message came from
topic โค๏ธ String mytopic Topic name; you'll likely want to filter for a specific topic
muted Boolean true Indicates whether the subscription was muted in the app
muted_str String (true or false) true Same as muted, but as string true or false
time Int 1635528741 Message date time, as Unix time stamp
title String Some title Message title; may be empty if not set
message โค๏ธ String Some message Message body; this is likely what you're interested in
message_bytes ByteArray (binary data) Message body as binary data
encoding๏ธ String - Message encoding (empty or "base64")
tags String tag1,tag2,.. Comma-separated list of tags
tags_map String 0=tag1,1=tag2,.. Map of tags to make it easier to map first, second, ... tag
priority Int (between 1-5) 4 Message priority with 1=min, 3=default and 5=max

Send messages using intents

To send messages from other apps (such as MacroDroid
and Tasker), you can
broadcast an intent with the io.heckel.ntfy.SEND_MESSAGE action. The ntfy Android app will forward the intent as a HTTP
POST request to publish a message. This is primarily useful for apps that do not support HTTP POST/PUT
(like MacroDroid). In Tasker, you can simply use the "HTTP Request" action, which is a little easier and also works if
ntfy is not installed.

Here's what that looks like:

text
[](../../static/img/android-screenshot-macrodroid-send-macro.png)
[](../../static/img/android-screenshot-macrodroid-send-action.png)
[](../../static/img/android-screenshot-tasker-profile-send.png)
[](../../static/img/android-screenshot-tasker-task-edit-post.png)
[](../../static/img/android-screenshot-tasker-action-http-post.png)

The following intent extras are supported when for the intent with the io.heckel.ntfy.SEND_MESSAGE action:

Extra name Required Type Example Description
base_url - String https://ntfy.sh Root URL of the ntfy server this message came from, defaults to https://ntfy.sh
topic โค๏ธ โœ” String mytopic Topic name; you must set this
title - String Some title Message title; may be empty if not set
message โค๏ธ โœ” String Some message Message body; you must set this
tags - String tag1,tag2,.. Comma-separated list of tags
priority - String or Int (between 1-5) 4 Message priority with 1=min, 3=default and 5=max

iPhone/iOS

I almost feel devious for putting the Download on the App Store button on this page. Currently, there is no iOS app
for ntfy, but it's in the works. You can track the status on GitHub.


File: docs/subscribe/pwa.md

Using the progressive web app (PWA)

While ntfy doesn't have a native desktop app, it is built as a progressive web app (PWA)
and thus can be installed on both desktop and mobile devices.

This gives it its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux, home screen icon on iOS, and
launcher icon on Android), a standalone window, push notifications, and an app badge with the unread notification count.

Web app installation is supported on (see compatibility table for details):

  • Chrome: Android, Windows, Linux, macOS
  • Safari: iOS 16.4+, macOS 14+
  • Firefox: Android, as well as on Windows/Linux via an extension
  • Edge: Windows

Note that for self-hosted servers, Web Push must be configured for the PWA to work.

Installation

Chrome on Desktop

To install and register the web app via Chrome, click the "install app" icon. After installation, you can find the app in your
app drawer:

text
[](../../static/img/pwa-install.png)
[](../../static/img/pwa.png) 
[](../../static/img/pwa-badge.png)

Safari on macOS

To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.

text
[](../../static/img/pwa-install-macos-safari-add-to-dock.png)

Chrome/Firefox on Android

For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
and on your home screen.

text
[](../../static/img/pwa-install-chrome-android.jpg)
[](../../static/img/pwa-install-chrome-android-menu.jpg)
[](../../static/img/pwa-install-chrome-android-popup.jpg)

For Firefox, select "Install" in the menu, and then click "Add" to add an icon to your home screen:

text
[](../../static/img/pwa-install-firefox-android-menu.jpg)
[](../../static/img/pwa-install-firefox-android-popup.jpg)

Safari on iOS

On iOS Safari, tap on the Share menu, then tap "Add to Home Screen":

text
[](../../static/img/pwa-install-safari-ios-button.jpg)
[](../../static/img/pwa-install-safari-ios-menu.jpg)
[](../../static/img/pwa-install-safari-ios-add-icon.jpg)

Background notifications

Background notifications via web push are enabled by default and cannot be turned off when the app is installed, as notifications would
not be delivered reliably otherwise. You can mute topics you don't want to receive notifications for.

On desktop, you generally need either your browser or the web app open to receive notifications, though the ntfy tab doesn't need to be
open. On mobile, you don't need to have the web app open to receive notifications. Look at the web docs
for a detailed breakdown.


File: docs/subscribe/web.md

Subscribe from the Web UI

You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, notifications will
pop up as desktop notifications
. Simply type in the topic name and click the Subscribe button. The browser will
keep a connection open and listen for incoming notifications.

To learn how to send messages, check out the publishing page.

text
[](../../static/img/web-detail.png) 
[](../../static/img/web-notification.png)
[](../../static/img/web-subscribe.png)

To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend,
is to pin the tab so that it's always open, but sort of out of the way:

{ width=500 }
Pin web app to move it out of the way

File: docs/config.md

Configuring the ntfy server

The ntfy server can be configured in three ways: using a config file (typically at /etc/ntfy/server.yml,
see server.yml), via command line arguments
or using environment variables.

Quick start

By default, simply running ntfy serve will start the server at port 80. No configuration needed. Batteries included ๐Ÿ˜€.
If everything works as it should, you'll see something like this:

text
$ ntfy serve
2021/11/30 19:59:08 Listening on :80

You can immediately start publishing messages, or subscribe via the Android app,
the web UI, or simply via curl or your favorite HTTP client. To configure
the server further, check out the config options table or simply type ntfy serve --help to
get a list of command line options.

Example config

!!! info
Definitely check out the server.yml file. It contains examples and detailed descriptions of all the settings.
You may also want to look at how ntfy.sh is configured in the ntfy-ansible repository.

The most basic settings are base-url (the external URL of the ntfy server), the HTTP/HTTPS listen address (listen-http
and listen-https), and socket path (listen-unix). All the other things are additional features.

Here are a few working sample configs using a /etc/ntfy/server.yml file:

=== "server.yml (HTTP-only, with cache + attachments)"
yaml base-url: "http://ntfy.example.com" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments"

=== "server.yml (HTTP+HTTPS, with cache + attachments)"
yaml base-url: "http://ntfy.example.com" listen-http: ":80" listen-https: ":443" key-file: "/etc/letsencrypt/live/ntfy.example.com.key" cert-file: "/etc/letsencrypt/live/ntfy.example.com.crt" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments"

=== "server.yml (behind proxy, with cache + attachments)"
yaml base-url: "http://ntfy.example.com" listen-http: ":2586" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments" behind-proxy: true

=== "server.yml (PostgreSQL, behind proxy)"
yaml base-url: "https://ntfy.example.com" listen-http: ":2586" database-url: "postgres://ntfy:[email protected]:5432/ntfy?sslmode=require" attachment-cache-dir: "/var/cache/ntfy/attachments" behind-proxy: true auth-default-access: "deny-all"

=== "server.yml (ntfy.sh config)"
``` yaml
# All the things: Behind a proxy, Firebase, cache, attachments,
# SMTP publishing & receiving

text
base-url: "https://ntfy.sh"
listen-http: "127.0.0.1:2586"
firebase-key-file: "/etc/ntfy/firebase.json"
cache-file: "/var/cache/ntfy/cache.db"
behind-proxy: true
attachment-cache-dir: "/var/cache/ntfy/attachments"
smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587"
smtp-sender-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "[email protected]"
smtp-server-listen: ":25"
smtp-server-domain: "ntfy.sh"
smtp-server-addr-prefix: "ntfy-"
keepalive-interval: "45s"
```

Alternatively, you can also use command line arguments or environment variables to configure the server. Here's an example
using Docker Compose (i.e. docker-compose.yml):

=== "Docker Compose (w/ auth, cache, attachments)"
yaml services: ntfy: image: binwiederhier/ntfy restart: unless-stopped environment: NTFY_BASE_URL: http://ntfy.example.com NTFY_CACHE_FILE: /var/lib/ntfy/cache.db NTFY_AUTH_FILE: /var/lib/ntfy/auth.db NTFY_AUTH_DEFAULT_ACCESS: deny-all NTFY_AUTH_USERS: 'phil:$$2a$$10$$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' # Must escape '$' as '$$' NTFY_BEHIND_PROXY: true NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments NTFY_ENABLE_LOGIN: true volumes: - ./:/var/lib/ntfy ports: - 80:80 command: serve

=== "Docker Compose (w/ auth, cache, web push, iOS)"
yaml services: ntfy: image: binwiederhier/ntfy restart: unless-stopped environment: NTFY_BASE_URL: http://ntfy.example.com NTFY_CACHE_FILE: /var/lib/ntfy/cache.db NTFY_AUTH_FILE: /var/lib/ntfy/auth.db NTFY_AUTH_DEFAULT_ACCESS: deny-all NTFY_BEHIND_PROXY: true NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments NTFY_ENABLE_LOGIN: true NTFY_UPSTREAM_BASE_URL: https://ntfy.sh NTFY_WEB_PUSH_PUBLIC_KEY: NTFY_WEB_PUSH_PRIVATE_KEY: NTFY_WEB_PUSH_FILE: /var/lib/ntfy/webpush.db NTFY_WEB_PUSH_EMAIL_ADDRESS: volumes: - ./:/var/lib/ntfy ports: - 8093:80 command: serve

Config generator

This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point. Please refer to the relevant sections in the doc for more details.

Open config generator

The config generator helps you create a custom config for your self-hosted ntfy instance. Click to open.

Config generatorBETA
This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point.

Reset
×

Edit
Preview

General
Database
Users
Message Cache
Attachments
Web Push
Email






Configure
Configure
Configure
Configure
Configure
Configure


Configure user management, access control, and provisioned users/ACLs. See access control for details.





  • Add user

  • Add ACL

  • Add token

Configure the message cache to allow devices to retrieve missed notifications. See message cache for details.

Allow users to upload and attach files to notifications. See attachments for details.

Enable browser push notifications via the Web Push API. VAPID keys are generated automatically. See web push for details.


Regenerate keys

Configure outgoing email notifications and/or incoming email publishing. See email notifications and email publishing for details.


Configure the PostgreSQL connection. See PostgreSQL for details.

server.yml
docker-compose.yml
Env variables

Configure options on the left to generate your config...

Database options

ntfy uses a database for storing messages (message cache), users and access control, and web push subscriptions.
You can choose between SQLite and PostgreSQL as the database backend.

SQLite

By default, ntfy uses SQLite with separate database files for each store. This is the simplest setup and requires
no external dependencies:

  • cache-file: Database file for the message cache.
  • auth-file: Database file for authentication and access control. If set, enables auth.
  • web-push-file: Database file for web push subscriptions.

PostgreSQL

As an alternative, you can configure ntfy to use PostgreSQL for all database-backed stores by setting the
database-url option to a PostgreSQL connection string.

When database-url is set, ntfy will use PostgreSQL for the message cache,
access control, and web push subscriptions instead of SQLite. The cache-file,
auth-file, and web-push-file options must not be set in this case.

Note that setting database-url implicitly enables authentication and access control (equivalent to setting
auth-file with SQLite). The default access is read-write, so anonymous users can still read and write to all
topics. To restrict access, set auth-default-access to deny-all (see access control).

You can also set this via the environment variable NTFY_DATABASE_URL or the command line flag --database-url.

To offload read-heavy queries from the primary database, you can optionally configure one or more read replicas
using the database-replica-urls option. When configured, non-critical read-only queries (e.g. fetching messages, checking access permissions, etc)
are distributed across the replicas using round-robin, while all writes and correctness-critical reads continue to go
to the primary. If a replica becomes unhealthy, ntfy automatically falls back to the primary until the replica recovers.
You can also set this via the environment variable NTFY_DATABASE_REPLICA_URLS (comma-separated) or the command line
flag --database-replica-urls.

Examples:

=== "Simple"
yaml database-url: "postgres://user:pass@host:5432/ntfy"

=== "With SSL and pool tuning"
yaml database-url: "postgres://user:pass@host:5432/ntfy?sslmode=require&pool_max_conns=50&pool_conn_max_idle_time=5m"

=== "With CA certificate"
yaml database-url: "postgres://user:pass@host:25060/ntfy?sslmode=require&sslrootcert=/etc/ntfy/db-ca-cert.pem&pool_max_conns=30"

=== "With read replicas"
yaml database-url: "postgres://user:pass@primary:5432/ntfy?sslmode=require&sslrootcert=/etc/ntfy/db-ca-cert.pem&pool_max_conns=30" database-replica-urls: - "postgres://user:pass@replica1:5432/ntfy?sslmode=require&sslrootcert=/etc/ntfy/db-ca-cert.pem&pool_max_conns=30" - "postgres://user:pass@replica2:5432/ntfy?sslmode=require&sslrootcert=/etc/ntfy/db-ca-cert.pem&pool_max_conns=30"

The database URL supports the standard PostgreSQL connection parameters
as query parameters, such as sslmode, connect_timeout, sslcert, sslkey, sslrootcert, and application_name.
See the pgx driver documentation for the full list of supported parameters.

In addition, ntfy supports the following custom query parameters to tune the connection pool (these apply to both
the primary and replica URLs):

Parameter Default Description
pool_max_conns 10 Maximum number of open connections to the database
pool_max_idle_conns - Maximum number of idle connections in the pool
pool_conn_max_lifetime - Maximum amount of time a connection may be reused (Go duration, e.g. 5m, 1h)
pool_conn_max_idle_time - Maximum amount of time a connection may be idle (Go duration, e.g. 30s, 5m)

Message cache

If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
of time is important to allow phones and other devices with brittle Internet connections to be able to retrieve
notifications that they may have missed.

By default, ntfy keeps messages in-memory for 12 hours, which means that cached messages do not survive an application
restart
. You can override this behavior by setting cache-file (SQLite) or database-url (PostgreSQL).

  • cache-duration: defines the duration for which messages are stored in the cache (default is 12h).

You can also entirely disable the cache by setting cache-duration to 0. When the cache is disabled, messages are only
passed on to the connected subscribers, but never stored on disk or even kept in memory longer than is needed to forward
the message to the subscribers.

Subscribers can retrieve cached messaging using the poll=1 parameter, as well as the
since= parameter.

Attachments

If desired, you may allow users to upload and attach files to notifications. To enable
this feature, you have to configure an attachment storage backend and a base URL (base-url). Attachments can be stored
either on the local filesystem or in an S3-compatible object store, both using the attachment-cache-dir option.
Once configured, you can upload attachments via PUT.

By default, attachments are stored for only 3 hours. The main reason for this is to avoid legal issues
and such when hosting user controlled content. Typically, this is more than enough time for the user (or the auto download
feature) to download the file. You can increase this time by purchasing ntfy Pro via the web app.

The following config options are relevant to attachments:

  • base-url is the root URL for the ntfy server; this is needed for the generated attachment URLs
  • attachment-cache-dir is the cache directory for attached files, or an S3 URL for object storage
  • attachment-total-size-limit is the size limit of the attachment storage (default: 5G)
  • attachment-file-size-limit is the per-file attachment size limit (e.g. 300k, 2M, 100M, default: 15M)
  • attachment-expiry-duration is the duration after which uploaded attachments will be deleted (e.g. 3h, 20h, default: 3h)

!!! warning
ntfy takes full control over the attachment directory or S3 bucket. Files that match the message ID format without
entries in the message table will be deleted. Do not use a directory or S3 bucket that is also used for something else.

Please also refer to the rate limiting settings below, specifically visitor-attachment-total-size-limit
and visitor-attachment-daily-bandwidth-limit. Setting these conservatively is necessary to avoid abuse.

Filesystem storage

Here's an example config using the local filesystem for attachment storage:

=== "/etc/ntfy/server.yml (minimal)"
yaml base-url: "https://ntfy.example.com" attachment-cache-dir: "/var/cache/ntfy/attachments"

=== "/etc/ntfy/server.yml (all options)"
yaml base-url: "https://ntfy.example.com" attachment-cache-dir: "/var/cache/ntfy/attachments" attachment-total-size-limit: "5G" attachment-file-size-limit: "15M" attachment-expiry-duration: "3h" visitor-attachment-total-size-limit: "100M" visitor-attachment-daily-bandwidth-limit: "500M"

S3 storage

As an alternative to the local filesystem, you can store attachments in an S3-compatible object store (e.g. AWS S3,
DigitalOcean Spaces). This is useful for HA/cloud deployments where you don't want to rely on local disk storage.
To use an S3-compatible storage for attachments, set attachment-cache-dir to an S3 URL with the following format:

text
s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]

Here are a few examples:

=== "/etc/ntfy/server.yml (DigitalOcean Spaces)"
yaml base-url: "https://ntfy.example.com" attachment-cache-dir: "s3://ACCESS_KEY:SECRET_KEY@my-bucket/attachments?region=nyc3&endpoint=https://nyc3.digitaloceanspaces.com&disable_http2=true"

=== "/etc/ntfy/server.yml (AWS S3)"
yaml base-url: "https://ntfy.example.com" attachment-cache-dir: "s3://ACCESS_KEY:SECRET_KEY@my-bucket/attachments?region=us-east-1"

=== "/etc/ntfy/server.yml (custom endpoint)"
yaml base-url: "https://ntfy.example.com" attachment-cache-dir: "s3://ACCESS_KEY:SECRET_KEY@my-bucket/attachments?region=us-east-1&endpoint=https://s3.example.com"

Note that the access key and secret key may have to be URL encoded. For instance, a secret key YmxhY+mxhYmxhC (note the +) should
be encoded as YmxhY%2BmxhYmxhC (note the %2B), so the URL would be s3://ACCESS_KEY:YmxhY%2BmxhYmxhC@my-bucket/attachments....

If you experience upload failures with HTTP/2 stream errors (common with DigitalOcean Spaces and some other S3-compatible providers),
add &disable_http2=true to force HTTP/1.1 connections.

!!! info
ntfy.sh is hosted and sponsored by DigitalOcean. I can highly recommend their public cloud offering. It's been rock solid
for 4 years. They offer an S3-compatible storage for $5/month and 250 GB of storage, with 1 TiB of bandwidth.
Also, if you use this referral link, you can get $200 credit.

For AWS S3, the IAM user needs the following permissions on the bucket:

json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads"
            ],
            "Resource": "arn:aws:s3:::BUCKET_NAME"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:AbortMultipartUpload"
            ],
            "Resource": "arn:aws:s3:::BUCKET_NAME/*"
        }
    ]
}

Access control

By default, the ntfy server is open for everyone, meaning everyone can read and write to any topic (this is how
ntfy.sh is configured). To restrict access to your own server, you can optionally configure authentication and authorization.

ntfy's auth implements two roles (user and admin) and per-topic read and write permissions using an
access control list (ACL). Access control entries can be applied
to users as well as the special everyone user (*), which represents anonymous API access.

To set up auth, configure the following options:

  • auth-file is the user/access database (SQLite); it is created automatically if it doesn't already exist; suggested
    location /var/lib/ntfy/user.db (easiest if deb/rpm package is used). Alternatively, if database-url is set,
    auth is automatically enabled using PostgreSQL (see database options).
  • auth-default-access defines the default/fallback access if no access control entry is found; it can be
    set to read-write (default), read-only, write-only or deny-all. If you are setting up a private instance,
    you'll want to set this to deny-all
    (see private instance example).

Once configured, you can use

These commands directly edit the auth database (as defined in auth-file), so they only work on the server,
and only if the user accessing them has the right permissions.

Users and roles

Users can be added to the ntfy user database in two different ways

  • Using the CLI: Using the ntfy user command, you can manually add/update/remove users.
  • In the config: You can provision users in the server.yml file via auth-users key.

Users via the CLI

The ntfy user command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles (user or admin). In practice, you'll often just create one admin
user with ntfy user add --role=admin ... and be done with all this (see example below).

Roles:

  • Role user (default): Users with this role have no special permissions. Manage access using ntfy access
    (see below).
  • Role admin: Users with this role can read/write to all topics. Granular access control is not necessary.

Example commands (type ntfy user --help or ntfy user COMMAND --help for more details):

text
ntfy user list                     # Shows list of users (alias: 'ntfy access')
ntfy user add phil                 # Add regular user phil  
ntfy user add --role=admin phil    # Add admin user phil
ntfy user del phil                 # Delete user phil
ntfy user change-pass phil         # Change password for user phil
ntfy user change-role phil admin   # Make user phil an admin
ntfy user change-tier phil pro     # Change phil's tier to "pro"
ntfy user hash                     # Generate password hash, use with auth-users config option

Users via the config

As an alternative to manually creating users via the ntfy user CLI command, you can provision users declaratively in
the server.yml file by adding them to the auth-users array. This is useful for general admins, or if you'd like to
deploy your ntfy server via Docker/Ansible without manually editing the database.

The auth-users option is a list of users that are automatically created/updated when the server starts. Users
previously defined in the config but later removed will be deleted. Each entry is defined in the format ::<role>.

Here's an example with two users: phil is an admin, ben is a regular user.

=== "Declarative users in /etc/ntfy/server.yml"
yaml auth-file: "/var/lib/ntfy/user.db" auth-users: - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin" - "ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user"

=== "Declarative users via env variables"
# Comma-separated list, use single quotes to avoid issues with the bcrypt hash NTFY_AUTH_FILE='/var/lib/ntfy/user.db' NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user'

The password hash can be created using ntfy user hash or an online bcrypt generator (though
note that you're putting your password in an untrusted website).

!!! important
Users added declaratively via the config file are marked in the database as "provisioned users". Removing users
from the config file will delete them from the database the next time ntfy is restarted.

text
Also, users that were originally manually created will be "upgraded" to be provisioned users if they are added to
the config. Adding a user manually, then adding it to the config, and then removing it from the config will hence
lead to the **deletion of that user**.

Access control list (ACL)

The access control list (ACL) manages access to topics for non-admin users, and for anonymous access (everyone/*).
Each entry represents the access permissions for a user to a specific topic or topic pattern. Entries can be created in
two different ways:

  • Using the CLI: Using the ntfy access command, you can manually edit the access control list.
  • In the config: You can provision ACL entries in the server.yml file via auth-access key.

ACL entries via the CLI

The ACL can be displayed or modified with the ntfy access command:

text
ntfy access                            # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME                   # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION  # Allow/deny access for USERNAME to TOPIC

A USERNAME is an existing user, as created with ntfy user add (see users and roles), or the
anonymous user everyone or *, which represents clients that access the API without username/password.

A TOPIC is either a specific topic name (e.g. mytopic, or phil_alerts), or a wildcard pattern that matches any
number of topics (e.g. alerts_* or ben-*). Only the wildcard character * is supported. It stands for zero to any
number of characters.

A PERMISSION is any of the following supported permissions:

  • read-write (alias: rw): Allows publishing messages to the given topic, as well as
    subscribing and reading messages
  • read-only (aliases: read, ro): Allows only subscribing and reading messages, but not publishing to the topic
  • write-only (aliases: write, wo): Allows only publishing to the topic, but not subscribing to it
  • deny (alias: none): Allows neither publishing nor subscribing to a topic

Example commands (type ntfy access --help for more details):

text
ntfy access                        # Shows entire access control list
ntfy access phil                   # Shows access for user phil
ntfy access phil mytopic rw        # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw    # Allow anonymous read-write access to mytopic
ntfy access everyone "up*" write   # Allow anonymous write-only access to topics "up..."
ntfy access --reset                # Reset entire access control list
ntfy access --reset phil           # Reset all access for user phil
ntfy access --reset phil mytopic   # Reset access for user phil and topic mytopic

Example ACL:

text
$ ntfy access
user phil (admin)
- read-write access to all topics (admin role)
user ben (user)
- read-write access to topic garagedoor
- read-write access to topic alerts*
- read-only access to topic furnace
user * (anonymous)
- read-only access to topic announcements
- read-only access to topic server-stats
- no access to any (other) topics (server config)

In this example, phil has the role admin, so he has read-write access to all topics (no ACL entries are necessary).
User ben has three topic-specific entries. He can read, but not write to topic furnace, and has read-write access
to topic garagedoor and all topics starting with the word alerts (wildcards). Clients that are not authenticated
(called */everyone) only have read access to the announcements and server-stats topics.

ACL entries via the config

As an alternative to manually creating ACL entries via the ntfy access CLI command, you can provision access control
entries declaratively in the server.yml file by adding them to the auth-access array, similar to the auth-users
option (see users via the config.

The auth-access option is a list of access control entries that are automatically created/updated when the server starts.
When entries are removed, they are deleted from the database. Each entry is defined in the format :<topic-pattern>:<access>.

The `` can be any existing, provisioned user as defined in the auth-users section (see users via the config),
or everyone/* for anonymous access. The <topic-pattern> can be a specific topic name or a pattern with wildcards (*). The
<access> can be one of the following:

  • read-write or rw: Allows both publishing to and subscribing to the topic
  • read-only, read, or ro: Allows only subscribing to the topic
  • write-only, write, or wo: Allows only publishing to the topic
  • deny-all, deny, or none: Denies all access to the topic

Here's an example with several ACL entries:

=== "Declarative ACL entries in /etc/ntfy/server.yml"
yaml auth-file: "/var/lib/ntfy/user.db" auth-users: - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user" - "ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user" auth-access: - "phil:mytopic:rw" - "ben:alerts-*:rw" - "ben:system-logs:ro" - "*:announcements:ro" # or: "everyone:announcements,ro"

=== "Declarative ACL entries via env variables"
# Comma-separated list NTFY_AUTH_FILE='/var/lib/ntfy/user.db' NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user' NTFY_AUTH_ACCESS='phil:mytopic:rw,ben:alerts-*:rw,ben:system-logs:ro,*:announcements:ro'

In this example, the auth-users section defines two users, phil and ben. The auth-access section defines
access control entries for these users. phil has read-write access to the topic mytopic, while ben has read-write
access to all topics starting with alerts- and read-only access to the topic system-logs. The last entry allows
anonymous users (i.e. clients that do not authenticate) to read the announcements topic.

Access tokens

In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful
to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may
want to use a dedicated token to publish from your backup host, and one from your home automation system.

!!! info
As of today, access tokens grant users full access to the user account. Aside from changing the password,
and deleting the account, every action can be performed with a token. Granular access tokens are on the roadmap,
but not yet implemented.

You can create access tokens in two different ways:

  • Using the CLI: Using the ntfy token command, you can manually add/update/remove tokens.
  • In the config: You can provision access tokens in the server.yml file via auth-tokens key.

Tokens via the CLI

The ntfy token command can be used to manage access tokens for users. Tokens can have labels, and they can expire
automatically (or never expire). Each user can have up to 60 tokens (hardcoded).

Example commands (type ntfy token --help or ntfy token COMMAND --help for more details):

text
ntfy token list                      # Shows list of tokens for all users
ntfy token list phil                 # Shows list of tokens for user phil
ntfy token add phil                  # Create token for user phil which never expires
ntfy token add --expires=2d phil     # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2sxr...  # Delete token
ntfy token generate                  # Generate random token, can be used in auth-tokens config option

Creating an access token:

text
$ ntfy token add --expires=30d --label="backups" phil
$ ntfy token list
user phil
- tk_7eevizlsiwf9yi4uxsrs83r4352o0 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST

Once an access token is created, you can use it to authenticate against the ntfy server, e.g. when you publish or
subscribe to topics
. To learn how, check out authenticate via access tokens.

Tokens via the config

Access tokens can be pre-provisioned in the server.yml configuration file using the auth-tokens config option.
This is useful for automated setups, Docker environments, or when you want to define tokens declaratively.

The auth-tokens option is a list of access tokens that are automatically created/updated when the server starts.
When entries are removed, they are deleted from the database. Each entry is defined in the format :<token>[:<label>].

The `` must be an existing, provisioned user, as defined in the auth-users section (see users via the config).
The <token> is a valid access token, which must start with tk_ and be 32 characters long (including the prefix). You can generate
random tokens using the ntfy token generate command. The optional <label> is a human-readable label for the token,
which can be used to identify it later.

Once configured, these tokens can be used to authenticate API requests just like tokens created via the CLI.
For usage examples, see authenticate via access tokens.

Here's an example:

=== "Declarative tokens in /etc/ntfy/server.yml"
yaml auth-file: "/var/lib/ntfy/user.db" auth-users: - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin" - "backup-service:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user" auth-tokens: - "phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76" - "backup-service:tk_f099we8uzj7xi5qshzajwp6jffvkz:Backup script"

=== "Declarative tokens via env variables"
# Comma-separated list NTFY_AUTH_FILE='/var/lib/ntfy/user.db' NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,backup-service:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user' NTFY_AUTH_TOKENS='phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76,backup-service:tk_f099we8uzj7xi5qshzajwp6jffvkz:Backup script'

In this example, the auth-users section defines two users, phil and backup-service. The auth-tokens section
defines access tokens for these users. phil has a token tk_3gd7d2yftt4b8ixyfe9mnmro88o76, while backup-service
has a token tk_f099we8uzj7xi5qshzajwp6jffvkz with the label "Backup script".

Example: Private instance

The easiest way to configure a private instance is to set auth-default-access to deny-all in the server.yml,
and to configure users in the auth-users section (see users via the config),
access control entries in the auth-access section (see ACL entries via the config),
and access tokens in the auth-tokens section (see access tokens via the config).

Here's an example that defines a single admin user phil with the password mypass, and a regular user backup-script
with the password backup-script. The admin user has full access to all topics, while regular user can only
access the backups topic with read-write permissions. phil has a token tk_3gd7d2yftt4b8ixyfe9mnmro88o76
with the label "My personal token". The auth-default-access is set to deny-all, which means
that all other users and anonymous access are denied by default.

=== "Config via /etc/ntfy/server.yml"
yaml auth-file: "/var/lib/ntfy/user.db" auth-default-access: "deny-all" auth-users: - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin" - "backup-script:$2a$10$/ehiQt.w7lhTmHXq.RNsOOkIwiPPeWFIzWYO3DRxNixnWKLX8.uj.:user" auth-access: - "backup-script:backups:rw" auth-tokens: - "phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76:My personal token"

=== "Config via env variables"
yaml NTFY_AUTH_FILE='/var/lib/ntfy/user.db' NTFY_AUTH_DEFAULT_ACCESS='deny-all' NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,backup-script:$2a$10$/ehiQt.w7lhTmHXq.RNsOOkIwiPPeWFIzWYO3DRxNixnWKLX8.uj.:user' NTFY_AUTH_ACCESS='backup-script:backups:rw' NTFY_AUTH_TOKENS='phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76:My personal token'

Once you've done that, you can publish and subscribe using Basic Auth
with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password.

Here's a simple example (using the credentials of the phil user):

=== "Command line (curl)"
curl \ -u phil:mypass \ -d "Look ma, with auth" \ https://ntfy.example.com/mysecrets

=== "ntfy CLI"
ntfy publish \ -u phil:mypass \ ntfy.example.com/mysecrets \ "Look ma, with auth"

=== "HTTP"
``` http
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Basic cGhpbDpteXBhc3M=

text
Look ma, with auth
```

=== "JavaScript"
javascript fetch('https://ntfy.example.com/mysecrets', { method: 'POST', // PUT works too body: 'Look ma, with auth', headers: { 'Authorization': 'Basic cGhpbDpteXBhc3M=' } })

=== "Go"
go req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets", strings.NewReader("Look ma, with auth")) req.Header.Set("Authorization", "Basic cGhpbDpteXBhc3M=") http.DefaultClient.Do(req)

=== "Python"
python requests.post("https://ntfy.example.com/mysecrets", data="Look ma, with auth", headers={ "Authorization": "Basic cGhpbDpteXBhc3M=" })

=== "PHP"
php-inline file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([ 'http' => [ 'method' => 'POST', // PUT also works 'header' => 'Content-Type: text/plain\r\n' . 'Authorization: Basic cGhpbDpteXBhc3M=', 'content' => 'Look ma, with auth' ] ]));

Example: UnifiedPush

UnifiedPush requires that the application server (e.g. Synapse, Fediverse Server, โ€ฆ)
has anonymous write access to the topic used for push messages.
The topic names used by UnifiedPush all start with the up* prefix. Please refer to the
UnifiedPush documentation for more details.

To enable support for UnifiedPush for private servers (i.e. auth-default-access: "deny-all"), you should either
allow anonymous write access for the entire prefix or explicitly per topic:

=== "Prefix"
$ ntfy access '*' 'up*' write-only

=== "Explicitly"
$ ntfy access '*' upYzMtZGZiYTY5 write-only

E-mail notifications

To allow forwarding messages via e-mail, you can configure an SMTP server for outgoing messages. Once configured,
you can set the X-Email header to send messages via e-mail (e.g.
curl -d "hi there" -H "X-Email: [email protected]" ntfy.sh/mytopic).

!!! info
On ntfy.sh, anonymous email sending was disabled due to abuse. To use the email notification feature,
you must verify your email in the web app's Account section.

As of today, only SMTP servers with PLAIN auth and STARTLS are supported. To enable e-mail sending, you must set the
following settings:

  • base-url is the root URL for the ntfy server; this is needed for e-mail footer
  • smtp-sender-addr is the hostname:port of the SMTP server
  • smtp-sender-user and smtp-sender-pass are the username and password of the SMTP user
  • smtp-sender-from is the e-mail address of the sender
  • smtp-sender-verify is a flag that forces email recipient verification when enabled. If set to true,
    only verified email recipients can be used in the X-Email header.

Here's an example config using Amazon SES for outgoing mail (this is how it is
configured for ntfy.sh):

=== "/etc/ntfy/server.yml"
yaml base-url: "https://ntfy.sh" smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587" smtp-sender-user: "AKIDEADBEEFAFFE12345" smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG." smtp-sender-from: "[email protected]" smtp-sender-verify: true

By default, any user (including anonymous users) can send email notifications to any address. To require email
address verification, set smtp-sender-verify to true. When enabled, anonymous users cannot send emails, and
authenticated users can only send to literal email addresses they have verified in their account settings.

Regardless of this setting, a logged-in user can pass yes/true/1 as the X-Email value to send to their primary
verified address (falling back to their first verified address if no primary is designated). smtp-sender-verify only
governs whether arbitrary literal addresses are allowed.

Please also refer to the rate limiting settings below, specifically visitor-email-limit-burst
and visitor-email-limit-burst. Setting these conservatively is necessary to avoid abuse.

E-mail publishing

To allow publishing messages via e-mail, ntfy can run a lightweight SMTP server for incoming messages. Once configured,
users can send emails to a topic e-mail address (e.g. [email protected] or
[email protected]) to publish messages to a topic. This is useful for e-mail based integrations such as for
statuspage.io (though these days most services also support webhooks and HTTP calls).

To configure the SMTP server, you must at least set smtp-server-listen and smtp-server-domain:

  • smtp-server-listen defines the IP address and port the SMTP server will listen on, e.g. :25 or 1.2.3.4:25
  • smtp-server-domain is the e-mail domain, e.g. ntfy.sh (must be identical to MX record, see below)
  • smtp-server-addr-prefix is an optional prefix for the e-mail addresses to prevent spam. If set to ntfy-, for instance,
    only e-mails to [email protected] will be accepted. If this is not set, all emails to [email protected] will be
    accepted (which may obviously be a spam problem).

Here's an example config (this is how it is configured for ntfy.sh):

=== "/etc/ntfy/server.yml"
yaml smtp-server-listen: ":25" smtp-server-domain: "ntfy.sh" smtp-server-addr-prefix: "ntfy-"

In addition to configuring the ntfy server, you have to create two DNS records (an MX record
and a corresponding A record), so incoming mail will find its way to your server. Here's an example of how ntfy.sh is
configured (in Amazon Route 53):

{ width=600 }
DNS records for incoming mail

You can check if everything is working correctly by sending an email as raw SMTP via nc. Create a text file, e.g.
email.txt

text
EHLO example.com
MAIL FROM: [email protected]
RCPT TO: [email protected]
DATA
Subject: Email for you
Content-Type: text/plain; charset="UTF-8"

Hello from ๐Ÿ‡ฉ๐Ÿ‡ช
.

And then send the mail via nc like this. If you see any lines starting with 451, those are errors from the
ntfy server. Read them carefully.

text
$ cat email.txt | nc -N ntfy.sh 25
220 ntfy.sh ESMTP Service Ready
250-Hello example.com
...
250 2.0.0 Roger, accepting mail from 
250 2.0.0 I'll make sure <[email protected]> gets this

As for the DNS setup, be sure to verify that dig MX and dig A are returning results similar to this:

text
$ dig MX ntfy.sh +short 
10 mx1.ntfy.sh.
$ dig A mx1.ntfy.sh +short 
3.139.215.220

Local-only email

If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
anything).

=== "/etc/ntfy/server.yml"
yaml smtp-server-listen: ":25" smtp-server-domain: "example.com" smtp-server-addr-prefix: "ntfy-" # optional

Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
ntfy instance. Set the port to the value you defined in smtp-server-listen. Leave any username and password
fields empty. In the "From" address, pick anything (e.g., "[email protected]"); the value doesn't matter.
In the "To" address, put in an email address that follows this pattern: [topic]@[smtp-server-domain] (or
[smtp-server-addr-prefix][topic]@[smtp-server-domain] if you set smtp-server-addr-prefix).

So if you used example.com as the SMTP server domain, and you want to send a message to the email-alerts
topic, set the "To" address to [email protected]. If the topic has access restrictions, you will need
to include an access token in the "To" address, such as [email protected].

If the internal service lets you use define an email "Subject", it will become the title of the notification.
The body of the email will become the message of the notification.

Behind a proxy (TLS, etc.)

!!! warning
If you are running ntfy behind a proxy, you must set the behind-proxy flag. Otherwise, all visitors are
rate limited as if they are one.

It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), so you can provide TLS certificates
using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services.
Whatever your reasons may be, there are a few things to consider.

IP-based rate limiting

If you are running ntfy behind a proxy, you should set the behind-proxy flag. This will instruct the
rate limiting logic to use the header configured in proxy-forwarded-header (default is X-Forwarded-For)
as the primary identifier for a visitor, as opposed to the remote IP address.

If the behind-proxy flag is not set, all visitors will be counted as one, because from the perspective of the
ntfy server, they all share the proxy's IP address.

Relevant flags to consider:

  • behind-proxy makes it so that the real visitor IP address is extracted from the header defined in proxy-forwarded-header.
    Without this, the remote address of the incoming connection is used (default: false).
  • proxy-forwarded-header is the header to use to identify visitors (default: X-Forwarded-For). It may be a single IP address (e.g. 1.2.3.4),
    a comma-separated list of IP addresses (e.g. 1.2.3.4, 5.6.7.8), or an RFC 7239-style
    header (e.g. for=1.2.3.4;by=proxy.example.com, for=5.6.7.8).
  • proxy-trusted-hosts is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header
    to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
    the forwarded header (default: empty).
  • visitor-prefix-bits-ipv4 is the number of bits of the IPv4 address to use for rate limiting (default is 32, which is the entire
    IP address). In IPv4 environments, by default, a visitor's full IPv4 address is used as-is for rate limiting. This means that
    if someone publishes messages from multiple IP addresses, they will be counted as separate visitors. You can adjust this by setting the visitor-prefix-bits-ipv4 config option. To group visitors in a /24 subnet and count them as one, for instance,
    set it to 24. In that case, 1.2.3.4 and 1.2.3.99 are treated as the same visitor.
  • visitor-prefix-bits-ipv6 is the number of bits of the IPv6 address to use for rate limiting (default is 64, which is a /64 subnet).
    In IPv6 environments, by default, a visitor's IP address is truncated to the /64 subnet, meaning that 2001:db8:25:86:1::1 and
    2001:db8:25:86:2::1 are treated as the same visitor. Use the visitor-prefix-bits-ipv6 config option to adjust this behavior.
    See IPv6 considerations for more details.

=== "/etc/ntfy/server.yml (behind a proxy)"
yaml # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting # # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, # the visitor IP will be 1.2.3.4 (right-most address). # behind-proxy: true

=== "/etc/ntfy/server.yml (X-Client-IP header)"
yaml # Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting # # Example: If "X-Client-IP: 9.9.9.9" is set, # the visitor IP will be 9.9.9.9. # behind-proxy: true proxy-forwarded-header: "X-Client-IP"

=== "/etc/ntfy/server.yml (Forwarded header)"
yaml # Tell ntfy to use "Forwarded" header (RFC 7239) to identify visitors for rate limiting # # Example: If "Forwarded: for=1.2.3.4;by=proxy.example.com, for=9.9.9.9" is set, # the visitor IP will be 9.9.9.9. # behind-proxy: true proxy-forwarded-header: "Forwarded"

=== "/etc/ntfy/server.yml (multiple proxies)"
yaml # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting, # and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5 # # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, # the visitor IP will be 9.9.9.9 (right-most unknown address). # behind-proxy: true proxy-trusted-hosts: "1.2.3.0/24, 1.2.2.2, 2001:db8::/64"

=== "/etc/ntfy/server.yml (adjusted IPv4/IPv6 prefixes proxies)"
yaml # Tell ntfy to treat visitors as being in a /24 subnet (IPv4) or /48 subnet (IPv6) # as one visitor, so that they are counted as one for rate limiting. # # Example 1: If 1.2.3.4 and 1.2.3.5 publish a message, the visitor 1.2.3.0 will have # used 2 messages. # Example 2: If 2001:db8:2500:1::1 and 2001:db8:2500:2::1 publish a message, the visitor # 2001:db8:2500:: will have used 2 messages. # visitor-prefix-bits-ipv4: 24 visitor-prefix-bits-ipv6: 48

TLS/SSL

ntfy supports HTTPS/TLS by setting the listen-https config option. However, if you
are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).

I highly recommend using certbot. I use it with the dns-route53 plugin,
which lets you use AWS Route 53 as the challenge. That's much easier than using the
HTTP challenge. I've found this guide to
be incredibly helpful.

nginx/Apache2/caddy

For your convenience, here's a working config that'll help configure things behind a proxy. Be sure to enable WebSockets
by forwarding the Connection and Upgrade headers accordingly.

In this example, ntfy runs on :2586 and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
or the root domain:

=== "nginx (convenient)"
```
/* Detailed source-code truncated for AI context efficiency. */

text
=== "nginx (more secure)"
    ```
/* Detailed source-code truncated for AI context efficiency. */

=== "Apache >= 2.4.47"
```
# /etc/apache2/sites-*/ntfy.conf

text
<VirtualHost *:80>
    ServerName ntfy.sh

    # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
    # Use mod_proxy_http for websocket upgrade ('upgrade=websocket'), which requires Apache (httpd) >= 2.4.47.
    ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
    ProxyPassReverse / http://127.0.0.1:2586/

    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1

    # Higher than the max message size of 4096 bytes
    LimitRequestBody 102400
    
    # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want 
    # it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
    
        RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
    

</VirtualHost>

<VirtualHost *:443>
    ServerName ntfy.sh
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/ntfy.sh/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    # Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
    # Use mod_proxy_http for websocket upgrade ('upgrade=websocket'), which requires Apache (httpd) >= 2.4.47.
    ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
    ProxyPassReverse / http://127.0.0.1:2586/

    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1

    # Higher than the max message size of 4096 bytes 
    LimitRequestBody 102400

</VirtualHost>
```

=== "Apache < 2.4.47"
```
/* Detailed source-code truncated for AI context efficiency. */

text
=== "caddy"
    ```
    # Note that this config is most certainly incomplete. Please help out and let me know what's missing
    # via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue.
    # Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy 

    ntfy.sh, http://nfty.sh {
        reverse_proxy 127.0.0.1:2586

        # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
        # it to work with curl without the annoying https:// prefix
        @httpget {
            protocol http
            method GET
            path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/)
        }
        redir @httpget https://{host}{uri}
    }
    ```
	
=== "ferron"
    ``` kdl
    // /etc/ferron.kdl	
    // Note that this config is most certainly incomplete. Please help out and let me know what's missing
    // via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue.
    // Note: Ferron automatically handles both HTTP and WebSockets with proxy 

    ntfy.sh {
        auto_tls
        auto_tls_letsencrypt_production
        protocols "h1" "h2" "h3"

        proxy "http://127.0.0.1:2586"

        // Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
        // it to work with curl without the annoying https:// prefix

        no_redirect_to_https #true

        condition "is_get_topic" {
            is_equal "{method}" "GET"
            is_regex "{path}" "^/([-_a-z0-9]{0,64}$|docs/|static/)"
        }

        if "is_get_topic" {
              no_redirect_to_https #false
        }
    }
    ```

## Firebase (FCM)
!!! info
    Using Firebase is **optional** and only works if you modify and [build your own Android .apk](develop.md#android-app).
    For a self-hosted instance, it's easier to just not bother with FCM.

[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) is the Google approved way to send
push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a
[foreground service](https://developer.android.com/guide/components/foreground-services).

For the main host [ntfy.sh](https://ntfy.sh), the [ntfy Android app](subscribe/phone.md) uses Firebase to send messages
to the device. For other hosts, instant delivery is used and FCM is not involved.

To configure FCM for your self-hosted instance of the ntfy server, follow these steps:

1. Sign up for a [Firebase account](https://console.firebase.google.com/)
2. Create a Firebase app and download the key file (e.g. `myapp-firebase-adminsdk-...json`)
3. Place the key file in `/etc/ntfy`, set the `firebase-key-file` in `server.yml` accordingly and restart the ntfy server
4. Build your own Android .apk following [these instructions](develop.md#android-app)

Example:

If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.

This is optional and only required to support Android apps (which don't allow background services anymore).

firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"

text
## iOS instant notifications
Unlike Android, iOS heavily restricts background processing, which sadly makes it impossible to implement instant 
push notifications without a central server. 

To still support instant notifications on iOS through your self-hosted ntfy server, you have to forward so called `poll_request` 
messages to the main ntfy.sh server (or any upstream server that's APNS/Firebase connected, if you build your own iOS app),
which will then forward it to Firebase/APNS.

To configure it, simply set `upstream-base-url` like so:

``` yaml
upstream-base-url: "https://ntfy.sh"
upstream-access-token: "..." # optional, only if rate limits exceeded, or upstream server protected

If set, all incoming messages will publish a poll request to the configured upstream server, containing
the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.

If upstream-base-url is not set, notifications will still eventually get to your device, but delivery can take hours,
depending on the state of the phone. If you are using your phone, it shouldn't take more than 20-30 minutes though.

In case you're curious, here's an example of the entire flow:

  • In the iOS app, you subscribe to https://ntfy.example.com/mytopic
  • The app subscribes to the Firebase topic 6de73be8dfb7d69e... (the SHA256 of the topic URL)
  • When you publish a message to https://ntfy.example.com/mytopic, your ntfy server will publish a
    poll request to https://ntfy.sh/6de73be8dfb7d69e.... The request from your server to the upstream server
    contains only the message ID (in the X-Poll-ID header), and the SHA256 checksum of the topic URL (as upstream topic).
  • The ntfy.sh server publishes the poll request message to Firebase, which forwards it to APNS, which forwards it to your iOS device
  • Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it

Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl:

text
curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b
{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"}

Note that the self-hosted server literally sends the message New message for every message, even if your message
may be Some other message. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
it'll show New message as a popup.

Web Push

Web Push (RFC8030)
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
When enabled, the user can enable background notifications for their topics in the web app under Settings. Once enabled by the
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
forward it to the browser.

To configure Web Push, you need to generate and configure a VAPID keypair (via ntfy webpush keys),
a database to keep track of the browser's subscriptions, and an admin email address (you):

  • web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
  • web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
  • web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db (not required if database-url is set)
  • web-push-email-address is the admin email address send to the push provider, e.g. [email protected]
  • web-push-startup-queries is an optional list of queries to run on startup`
  • web-push-expiry-warning-duration defines the duration after which unused subscriptions are sent a warning (default is 55d)
  • web-push-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d)

Alternatively, you can use PostgreSQL instead of SQLite by setting database-url
(see PostgreSQL database).

Limitations:

  • Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A valid
    certificate is required, as service workers will not run on origins with untrusted certificates.

  • Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This
    is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.

To configure VAPID keys, first generate them:

sh
$ ntfy webpush keys
Web Push keys generated.
...

Then copy the generated values into your server.yml or use the corresponding environment variables or command line arguments:

yaml
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: [email protected]

The web-push-file is used to store the push subscriptions in a local SQLite database. Alternatively, if database-url
is set, subscriptions are stored in PostgreSQL and web-push-file is not required. Unused subscriptions will send out
a warning after 55 days, and will automatically expire after 60 days (default). If the gateway returns an error
(e.g. 410 Gone when a user has unsubscribed), subscriptions are also removed automatically.

The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.

Changing your public/private keypair is not recommended. Browsers only allow one server identity (public key) per origin, and
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.

Tiers

ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
daily message limits, attachment size, or make it possible for users to reserve topics. If payments are enabled,
tiers can be paid or unpaid, and users can upgrade/downgrade between them. If payments are disabled, then the only way
to switch between tiers is with the ntfy user change-tier command (see users and roles).

By default, newly created users have no tier, and all usage limits are read from the server.yml config file.
Once a user is associated with a tier, some limits are overridden based on the tier.

The ntfy tier command can be used to manage all available tiers. By default, there are no pre-defined tiers.

Example commands (type ntfy token --help or ntfy token COMMAND --help for more details):

text
ntfy tier add pro                     # Add tier with code "pro", using the defaults
ntfy tier change --name="Pro" pro     # Update the name of an existing tier
ntfy tier del starter                 # Delete an existing tier
ntfy user change-tier phil pro        # Switch user "phil" to tier "pro"

Creating a tier (full example):

text
ntfy tier add \
  --name="Pro" \
  --message-limit=10000 \
  --message-expiry-duration=24h \
  --email-limit=50 \
  --call-limit=10 \
  --reservation-limit=10 \
  --attachment-file-size-limit=100M \
  --attachment-total-size-limit=1G \
  --attachment-expiry-duration=12h \
  --attachment-bandwidth-limit=5G \
  --stripe-price-id=price_123456 \
  pro

Payments

ntfy supports paid tiers via Stripe as a payment provider. If payments are enabled,
users can register, login and switch plans in the web app. The web app will behave slightly differently if payments
are enabled (e.g. showing an upgrade banner, or "ntfy Pro" tags).

!!! info
The ntfy payments integration is very tailored to ntfy.sh and Stripe. I do not intend to support arbitrary use
cases.

To enable payments, sign up with Stripe, set the stripe-secret-key and stripe-webhook-key
config options:

  • stripe-secret-key is the key used for the Stripe API communication. Setting this values
    enables payments in the ntfy web app (e.g. Upgrade dialog). See API keys.
  • stripe-webhook-key is the key required to validate the authenticity of incoming webhooks from Stripe.
    Webhooks are essential to keep the local database in sync with the payment provider. See Webhooks.
  • billing-contact is an email address or website displayed in the "Upgrade tier" dialog to let people reach
    out with billing questions. If unset, nothing will be displayed.

In addition to setting these two options, you also need to define a Stripe webhook
for the customer.subscription.updated and customer.subscription.deleted event, which points
to https://ntfy.example.com/v1/account/billing/webhook.

Here's an example:

yaml
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
billing-contact: "[email protected]"

Phone calls

ntfy supports phone calls via Twilio as a call provider. If phone calls are enabled,
users can verify and add a phone number, and then receive phone calls when publishing a message using the X-Call header.
See publishing page for more details.

To enable Twilio integration, sign up with Twilio, purchase a phone number (Toll free numbers
are the easiest), and then configure the following options:

  • twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
  • twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
  • twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
  • twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
  • twilio-call-format is the custom Twilio markup (TwiML) to use for phone calls (optional)

After you have configured phone calls, create a tier with a call limit (e.g. ntfy tier create --call-limit=10 ...),
and then assign it to a user. Users may then use the X-Call header to receive a phone call when publishing a message.

To customize the message that is spoken out loud, set the twilio-call-format option with TwiML. The format is
rendered as a Go template, so you can use the following fields from the message:

  • {{.Topic}} is the topic name
  • {{.Message}} is the message body
  • {{.Title}} is the message title
  • {{.Tags}} is a list of tags
  • {{.Priority}} is the message priority
  • {{.Sender}} is the IP address or username of the sender

Here's an example:

=== "Custom TwiML (English)"
``` yaml
twilio-account: "AC12345beefbeef67890beefbeef122586"
twilio-auth-token: "affebeef258625862586258625862586"
twilio-phone-number: "+18775132586"
twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
twilio-call-format: |

text
<Say loop="3">
      Yo yo yo, you should totally check out this message for {{.Topic}}.
      {{ if eq .Priority 5 }}
        It's really really important, dude. So listen up!
      {{ end }}
      
      {{ if neq .Title "" }}
        Bro, it's titled: {{.Title}}.
      {{ end }}
      
      {{.Message}}
      
      That is all.
      
      You know who this message is from? It is from {{.Sender}}.
      
    </Say>
    <Say>See ya!</Say>
  </Response>
```

=== "Custom TwiML (German)"
``` yaml
twilio-account: "AC12345beefbeef67890beefbeef122586"
twilio-auth-token: "affebeef258625862586258625862586"
twilio-phone-number: "+18775132586"
twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
twilio-call-format: |

text
<Say loop="3" voice="alice" language="de-DE">
      Du hast eine Nachricht zum Thema {{.Topic}}.
      {{ if eq .Priority 5 }}
        Achtung. Die Nachricht ist sehr wichtig.
      {{ end }}
      
      {{ if neq .Title "" }}
        Titel der Nachricht: {{.Title}}.
      {{ end }}
      
      Nachricht:
      
      {{.Message}}
      
      Ende der Nachricht.
      
      Diese Nachricht wurde vom Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
      
    </Say>
    <Say voice="alice" language="de-DE">Alla mol!</Say>
  </Response>
```

Message limits

There are a few message limits that you can configure:

  • message-size-limit defines the max size of a message body. Please note message sizes >4K are not recommended,
    and largely untested
    . The Android/iOS and other clients may not work, or work properly. If FCM and/or APNS is used,
    the limit should stay 4K, because their limits are around that size. If you increase this size limit regardless,
    FCM and APNS will NOT work for large messages.
  • message-delay-limit defines the max delay of a message when using the "Delay" header and scheduled delivery.

Rate limiting

!!! info
Be aware that if you are running ntfy behind a proxy, you must set the behind-proxy flag.
Otherwise, all visitors are rate limited as if they are one.

By default, ntfy runs without authentication, so it is vitally important that we protect the server from abuse or overload.
There are various limits and rate limits in place that you can use to configure the server:

  • Global limit: A global limit applies across all visitors (IPs, clients, users)
  • Visitor limit: A visitor limit only applies to a certain visitor. A visitor is identified by its IP address
    (or the X-Forwarded-For header if behind-proxy is set). All config options that start with the word visitor apply
    only on a per-visitor basis.

During normal usage, you shouldn't encounter these limits at all, and even if you burst a few requests or emails
(e.g. when you reconnect after a connection drop), it shouldn't have any effect.

General limits

Let's do the easy limits first:

  • global-topic-limit defines the total number of topics before the server rejects new topics. It defaults to 15,000.
  • visitor-subscription-limit is the number of subscriptions (open connections) per visitor. This value defaults to 30.

Request limits

In addition to the limits above, there is a requests/second limit per visitor for all sensitive GET/PUT/POST requests.
This limit uses a token bucket (using Go's rate package):

Each visitor has a bucket of 60 requests they can fire against the server (defined by visitor-request-limit-burst).
After the 60, new requests will encounter a 429 Too Many Requests response. The visitor request bucket is refilled at a rate of one
request every 5s (defined by visitor-request-limit-replenish)

  • visitor-request-limit-burst is the initial bucket of requests each visitor has. This defaults to 60.
  • visitor-request-limit-replenish is the rate at which the bucket is refilled (one request per x). Defaults to 5s.
  • visitor-request-limit-exempt-hosts is a comma-separated list of hostnames and IPs to be exempt from request rate
    limiting; hostnames are resolved at the time the server is started. Defaults to an empty list.

Message limits

By default, the number of messages a visitor can send is governed entirely by the request limit.
For instance, if the request limit allows for 15,000 requests per day, and all of those requests are POST/PUT requests
to publish messages, then that is the daily message limit.

To limit the number of daily messages per visitor, you can set visitor-message-daily-limit. This defines the number
of messages a visitor can send in a day. This counter is reset every day at midnight (UTC).

Attachment limits

Aside from the global file size and total attachment cache limits (see above), there are two relevant
per-visitor limits:

  • visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor. It defaults to 100M.
    The per-visitor storage is automatically decreased as attachments expire. External attachments (attached via X-Attach,
    see publishing docs) do not count here.
  • visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload bandwidth limit per visitor,
    including PUT and GET requests. This is to protect your precious bandwidth from abuse, since egress costs money in
    most cloud providers. This defaults to 500M.

E-mail limits

Similarly to the request limit, there is also an e-mail limit (only relevant if e-mail notifications
are enabled):

  • visitor-email-limit-burst is the initial bucket of emails each visitor has. This defaults to 16.
  • visitor-email-limit-replenish is the rate at which the bucket is refilled (one email per x). Defaults to 1h.

Topic creation limits

To mitigate topic-enumeration / squatting attacks (where a single source pokes thousands of guessable
topic names to inflate the server's in-memory topic map), there is a per-visitor limit on how many new
topics each visitor can cause to be created. Touching topics that already exist in memory does not consume
a token; only first-time insertions do.

  • visitor-topic-creation-limit-burst is the initial bucket of new-topic tokens. Set to 0 to disable
    the limit entirely. Defaults to 100.
  • visitor-topic-creation-limit-replenish is the rate at which the bucket is refilled (one new topic per x).
    Defaults to 1m.

Firebase limits

If Firebase is configured, all messages are also published to a Firebase topic (unless Firebase: no
is set). Firebase enforces its own limits
on how many messages can be published. Unfortunately these limits are a little vague and can change depending on the time
of day. In practice, I have only ever observed 429 Quota exceeded responses from Firebase if too many messages are published to
the same topic
.

In ntfy, if Firebase responds with a 429 after publishing to a topic, the visitor (= IP address) who published the message
is banned from publishing to Firebase for 10 minutes (not configurable). Because publishing to Firebase happens asynchronously,
there is no indication of the user that this has happened. Non-Firebase subscribers (WebSocket or HTTP stream) are not affected.
After the 10 minutes are up, messages forwarding to Firebase is resumed for this visitor.

If this ever happens, there will be a log message that looks something like this:

text
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor

IPv6 considerations

By default, rate limiting for IPv6 is done using the /64 subnet of the visitor's IPv6 address. This means that all visitors
in the same /64 subnet are treated as one visitor. This is done to prevent abuse, as IPv6 subnet assignments are typically
much larger than IPv4 subnets (and much cheaper), and it is common for ISPs to assign large subnets to their customers.

Other than that, rate limiting for IPv6 is done the same way as for IPv4, using the visitor's IP address or subnet to identify them.

There are two options to configure the number of bits used for rate limiting (for IPv4 and IPv6):

  • visitor-prefix-bits-ipv4 is number of bits of the IPv4 address to use for rate limiting (default: 32, full address)
  • visitor-prefix-bits-ipv6 is number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)

Subscriber-based rate limiting

By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment
size are all based on the visitor who publishes a message. Subscriber-based rate limiting is a way to use the rate limits
of a topic's subscriber, instead of the limits of the publisher.

If subscriber-based rate limiting is enabled, messages published on UnifiedPush topics (topics starting with up, e.g. up123456789012)
will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.

Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
requires read-write permission on the topic.

If this setting is enabled, publishing to UnifiedPush topics will lead to an HTTP 507 Insufficient Storage
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
visitor-message-daily-limit.

To enable subscriber-based rate limiting, set visitor-subscriber-rate-limiting: true.

!!! info
Due to a denial-of-service issue, support for the Rate-Topics
header was removed entirely. This is unfortunate, but subscriber-based rate limiting will still work for up* topics.

Tuning for scale

If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
if it's not behind a proxy, the ntfy server can keep about as many connections as the open file limit allows.
This limit is typically called nofile. Other than that, RAM and CPU are obviously relevant. You may also want to check
out this discussion on Reddit.

Depending on how you run it, here are a few limits that are relevant:

Message cache

By default, the message cache (defined by cache-file) uses the SQLite default settings, which means it
syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh,
the write-ahead log (WAL) should be enabled, and the sync mode should be adjusted.
See this article for details.

In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache
in batches, and asynchronously. This can be enabled with the cache-batch-size and cache-batch-timeout. If you start
seeing database locked messages in the logs, you should probably enable that.

Here's how ntfy.sh has been tuned in the server.yml file:

yaml
cache-batch-size: 25
cache-batch-timeout: "1s"
cache-startup-queries: |
    pragma journal_mode = WAL;
    pragma synchronous = normal;
    pragma temp_store = memory;
    pragma busy_timeout = 15000;
    vacuum;

For systemd services

If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
LimitNOFILE setting in the systemd unit. The default open files limit for ntfy.service is 10,000. You can override it
by creating a /etc/systemd/system/ntfy.service.d/override.conf file. As far as I can tell, /etc/security/limits.conf
is not relevant.

=== "/etc/systemd/system/ntfy.service.d/override.conf"
# Allow 20,000 ntfy connections (and give room for other file handles) [Service] LimitNOFILE=20500

Outside of systemd

If you're running outside systemd, you may want to adjust your /etc/security/limits.conf file to
increase the nofile setting. Here's an example that increases the limit to 5,000. You can find out the current setting
by running ulimit -n, or manually override it temporarily by running ulimit -n 50000.

=== "/etc/security/limits.conf"
# Increase open files limit globally * hard nofile 20500

Proxy limits (nginx, Apache2)

If you are running behind a proxy (e.g. nginx, Apache), the open files limit of the proxy is also
relevant. So if your proxy runs inside of systemd, increase the limits in systemd for the proxy. Typically, the proxy
open files limit has to be double the number of how many connections you'd like to support, because the proxy has
to maintain the client connection and the connection to ntfy.

=== "/etc/nginx/nginx.conf"
events { # Allow 40,000 proxy connections (2x of the desired ntfy connection count; # and give room for other file handles) worker_connections 40500; }

=== "/etc/systemd/system/nginx.service.d/override.conf"
# Allow 40,000 proxy connections (2x of the desired ntfy connection count; # and give room for other file handles) [Service] LimitNOFILE=40500

Banning bad actors (fail2ban)

If you put stuff on the Internet, bad actors will try to break them or break in. fail2ban
and nginx's ngx_http_limit_req_module module can be used
to ban client IPs if they misbehave. This is on top of the rate limiting inside the ntfy server.

Here's an example for how ntfy.sh is configured, following the instructions from two tutorials (here
and here):

=== "/etc/nginx/nginx.conf"
```
# Rate limit all IP addresses
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=45r/m;
}

text
# Alternatively, whitelist certain IP addresses
http {
  geo $limited {
    default 1;
    116.203.112.46/32 0;
    132.226.42.65/32 0;
    ...
  }
  map $limited $limitkey {
    1 $binary_remote_addr;
    0 "";
  }
  limit_req_zone $limitkey zone=one:10m rate=45r/m;
}
```

=== "/etc/nginx/sites-enabled/ntfy.sh"
# For each server/location block server { location / { limit_req zone=one burst=1000 nodelay; } }

=== "/etc/fail2ban/filter.d/nginx-req-limit.conf"
[Definition] failregex = limiting requests, excess:.* by zone.*client: <HOST> ignoreregex =

=== "/etc/fail2ban/jail.local"
[nginx-req-limit] enabled = true filter = nginx-req-limit action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp] logpath = /var/log/nginx/error.log findtime = 600 bantime = 14400 maxretry = 10

Note that if you run nginx in a container, append , chain=DOCKER-USER to the jail.local action. By default, the jail action chain
is INPUT, but FORWARD is used when using docker networks. DOCKER-USER, available when using docker, is part of the FORWARD
chain.

The official ntfy.sh server uses fail2ban to ban IPs. Check out ntfy.sh's Ansible fail2ban role for details. Ban actors are banned for 1 hour initially, and up to
4 hours at a time for repeated offenses. IPv4 addresses are banned individually, while IPv6 addresses are banned by their /56 prefix.

Ban-feed

In addition to the fail2ban setup above, ntfy can detect abusive visitors itself and write their IP
addresses to a file for fail2ban to ban from. ntfy keeps a per-prefix weighted "strike" budget, and
each rejected request costs strikes based on its response code -- the ntfy error code, or its HTTP
status (see ban-weights) -- so different kinds of rejection can be weighted differently or exempted
entirely. When a prefix exceeds the budget, ntfy appends the offending IP address to ban-file. Since
every line is already a confirmed offender, the fail2ban jail can ban on first sight (maxretry = 1).

  • ban-file is the file offenders are appended to. If it is not set, the ban-feed is disabled. Its
    parent directory must exist and be writable by ntfy. Be sure to rotate it (e.g. with logrotate and
    copytruncate) so it does not grow unbounded.
  • ban-window is the rolling window over which weighted strikes are counted, per IP prefix.
  • ban-threshold is the number of weighted strikes per ban-window before a prefix is written to
    ban-file. Each prefix has one shared budget, so it cannot be gamed by mixing error codes.
  • ban-weights assigns a strike weight per matcher key, formatted as KEY:WEIGHT. A key is an exact
    ntfy error code (42909), a code family (429*, 403*, 4*), a bare HTTP status (403, short for
    403*), or *. The longest matching key wins. A weight of 0 exempts a code entirely (it never
    counts toward a ban), useful to spare a specific code from a * catch-all. Heavier weights ban faster. If you do not
    include a * rule, any code that matches nothing defaults to weight 1 (i.e. it can be banned);
    set *:0 to exempt everything that is not explicitly weighted.

Only rejections (4xx/5xx) count towards a ban; successful requests never do. Because the budget
refills over ban-window, the trigger is a sustained rate: a prefix is only written out once it
exceeds ban-threshold / ban-window rejections per second (with the defaults, 100 / 10m = ~0.17/s).

Each line in ban-file has the format <RFC3339-timestamp> <http-code> <ntfy-code>, for example:

text
2026-01-15T20:56:32Z 1.2.3.4 1.2.3.4/32 429 42901
2026-01-15T20:56:32Z 2001:db8::abcd 2001:db8::/64 429 42909

is masked to the rate-limiting prefix (visitor-prefix-bits-ipv4/-ipv6) -- the
same unit ntfy rate-limits by. Have the fail2ban filter capture the bare `` (the action then
applies the prefix):

=== "server.yml"
yaml ban-file: "/var/log/ntfy/ban.log" ban-window: "10m" ban-threshold: 100 ban-weights: - "42909:10" # too many auth failures -> brute force, ban fast # everything else 4xx/5xx defaults to weight 1

=== "/etc/fail2ban/filter.d/ntfy-ban.conf"
[Definition] failregex = ^\S+ <HOST> \S+ \d+ \d+$ datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S ignoreregex =

=== "/etc/fail2ban/jail.d/ntfy-ban.local"
[ntfy-ban] enabled = true filter = ntfy-ban action = iptables-multiport[name=ntfy-ban, port="http,https", protocol=tcp] logpath = /var/log/ntfy/ban.log maxretry = 1 findtime = 1m bantime = 1h

IPv6 support

ntfy fully supports IPv6, though there are a few things to keep in mind.

  • Listening on an IPv6 address: By default, ntfy listens on :80 (IPv4-only). If you want to listen on an IPv6 address, you need to
    explicitly set the listen-http and/or listen-https options in your server.yml file to an IPv6 address, e.g. [::]:80. To listen on
    IPv4 and IPv6, you must run ntfy behind a reverse proxy, e.g. listen :80; listen [::]:80; in nginx.
  • Rate limiting: By default, ntfy uses the /64 subnet of the visitor's IPv6 address for rate limiting. This means that all visitors in the same /64
    subnet are treated as one visitor. If you want to change this, you can set the visitor-prefix-bits-ipv6 option in your server.yml file to a different
    value (e.g. 48 for /48 subnets). See IPv6 considerations and IP-based rate limiting for more details.
  • Banning IPs with fail2ban: By default, if you're using the iptables-multiport action, fail2ban bans individual IPv4 and IPv6 addresses via iptables and ip6tables. While this behavior is fine for IPv4, it is not for IPv6, because every host can technically have up to 2^64 addresses. Please ensure that your actionban and actionunban commands
    support IPv6 and also ban the entire prefix (e.g. /48). See Banning bad actors for details.

!!! info
The official ntfy.sh server supports IPv6. Check out ntfy.sh's Ansible repository for examples of how to
configure ntfy, nginx and fail2ban.

Health checks

A preliminary health check API endpoint is exposed at /v1/health. The endpoint returns a json response in the format shown below.
If a non-200 HTTP status code is returned or if the returned healthy field is false the ntfy service should be considered as unhealthy.

json
{"healthy":true}

See Installation for Docker for an example of how this could be used in a docker-compose environment.

Monitoring

If configured, ntfy can expose a /metrics endpoint for Prometheus, which can then be used to
create dashboards and alerts (e.g. via Grafana).

To configure the metrics endpoint, either set enable-metrics, or set the metrics-listen-http option to a dedicated
listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are
doing, and/or secure access to the endpoint in your reverse proxy.

  • enable-metrics enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)
  • metrics-listen-http moves the metrics endpoint to a dedicated [IP]:port, e.g. "10.0.1.1:9090" or ":9090". It
    implicitly enables metrics. If set, the metrics are served only on that dedicated port, and the default ntfy server
    does not serve /metrics, even if enable-metrics is also set.

=== "server.yml (Using default port)"
yaml enable-metrics: true

=== "server.yml (Using dedicated IP/port)"
yaml metrics-listen-http: "10.0.1.1:9090"

In Prometheus, an example scrape config would look like this:

=== "prometheus.yml"
yaml scrape_configs: - job_name: "ntfy" static_configs: - targets: ["10.0.1.1:9090"]

Here's an example Grafana dashboard built from the metrics (see Grafana JSON on GitHub):

[](../../static/img/grafana-dashboard.png)
ntfy Grafana dashboard

Profiling

ntfy can expose Go's net/http/pprof endpoints to support profiling of the ntfy server.
If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on http://:/debug/pprof/.
This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the profile-listen-http config option.

Logging & debugging

By default, ntfy logs to the console (stderr), with an info log level, and in a human-readable text format.

ntfy supports five different log levels, can also write to a file, log as JSON, and even supports granular
log level overrides for easier debugging. Some options (log-level and log-level-overrides) can be hot reloaded
by calling kill -HUP $pid or systemctl reload ntfy.

The following config options define the logging behavior:

  • log-format defines the output format, can be text (default) or json
  • log-file is a filename to write logs to. If this is not set, ntfy logs to stderr.
  • log-level defines the default log level, can be one of trace, debug, info (default), warn or error.
    Be aware that debug (and particularly trace) can be very verbose. Only turn them on briefly for debugging purposes.
  • log-level-overrides lets you override the log level if certain fields match. This is incredibly powerful
    for debugging certain parts of the system (e.g. only the account management, or only a certain visitor).
    This is an array of strings in the format:
    • field=value -> level to match a value exactly, e.g. tag=manager -> trace
    • field -> level to match any value, e.g. time_taken_ms -> debug

Logging config (good for production use):

yaml
log-level: info
log-format: json
log-file: /var/log/ntfy.log

Temporary debugging:
If something's not working right, you can debug/trace through what the ntfy server is doing by setting the log-level
to debug or trace. The debug setting will output information about each published message, but not the message
contents. The trace setting will also print the message contents.

Alternatively, you can set log-level-overrides for only certain fields, such as a visitor's IP address (visitor_ip),
a username (user_name), or a tag (tag). There are dozens of fields you can use to override log levels. To learn what
they are, either turn the log-level to trace and observe, or reference the source code.

Here's an example that will output only info log events, except when they match either of the defined overrides:

yaml
log-level: info
log-level-overrides:
  - "tag=manager -> trace"
  - "visitor_ip=1.2.3.4 -> debug"
  - "time_taken_ms -> debug"

!!! warning
The debug and trace log levels are very verbose, and using log-level-overrides has a
performance penalty. Only use it for temporary debugging.

You can also hot-reload the log-level and log-level-overrides by sending the SIGHUP signal to the process after
editing the server.yml file. You can do so by calling systemctl reload ntfy (if ntfy is running inside systemd),
or by calling kill -HUP $(pidof ntfy). If successful, you'll see something like this:

text
$ ntfy serve
2022/06/02 10:29:28 INFO Listening on :2586[http] :1025[smtp], log level is INFO
2022/06/02 10:29:34 INFO Partially hot reloading configuration ...
2022/06/02 10:29:34 INFO Log level is TRACE

Config options

Each config option can be set in the config file /etc/ntfy/server.yml (e.g. listen-http: :80) or as a
CLI option (e.g. --listen-http :80. Here's a list of all available options. Alternatively, you can set an environment
variable before running the ntfy command (e.g. export NTFY_LISTEN_HTTP=:80).

!!! info
All config options can also be defined in the server.yml file using underscores instead of dashes, e.g.
cache_duration and cache-duration are both supported. This is to support stricter YAML parsers that do
not support dashes.

Config option Env variable Format Default Description
base-url NTFY_BASE_URL URL - Public facing base URL of the service (e.g. https://ntfy.sh)
listen-http NTFY_LISTEN_HTTP [host]:port :80 Listen address for the HTTP web server
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
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).
database-url NTFY_DATABASE_URL string (connection URL) - PostgreSQL connection string (e.g. postgres://user:pass@host:5432/ntfy). If set, uses PostgreSQL for all database-backed stores (message cache, user manager, web push) instead of SQLite. See database options.
database-replica-urls NTFY_DATABASE_REPLICA_URLS list of strings (connection URLs) - PostgreSQL read replica connection strings. Non-critical read-only queries are distributed across replicas (round-robin) with automatic fallback to primary. Requires database-url.
cache-file NTFY_CACHE_FILE filename - If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See message cache.
cache-duration NTFY_CACHE_DURATION duration 12h Duration for which messages will be buffered before they are deleted. This is required to support the since=... and poll=1 parameter. Set this to 0 to disable the cache entirely.
cache-startup-queries NTFY_CACHE_STARTUP_QUERIES string (SQL queries) - SQL queries to run during database startup; this is useful for tuning and enabling WAL mode
cache-batch-size NTFY_CACHE_BATCH_SIZE int 0 Max size of messages to batch together when writing to message cache (if zero, writes are synchronous)
cache-batch-timeout NTFY_CACHE_BATCH_TIMEOUT duration 0s Timeout for batched async writes to the message cache (if zero, writes are synchronous)
auth-file NTFY_AUTH_FILE filename - Auth database file used for access control (SQLite). If set, enables authentication and access control. Not required if database-url is set. See access control.
auth-default-access NTFY_AUTH_DEFAULT_ACCESS read-write, read-only, write-only, deny-all read-write Default permissions if no matching entries in the auth database are found. Default is read-write.
auth-access-cache NTFY_AUTH_ACCESS_CACHE bool false Enables an in-memory ACL cache so authorization checks no longer hit the database. Only worth enabling on high-volume servers.
behind-proxy NTFY_BEHIND_PROXY bool false If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)
proxy-forwarded-header NTFY_PROXY_FORWARDED_HEADER string X-Forwarded-For Use specified header to determine visitor IP address (for rate limiting)
proxy-trusted-hosts NTFY_PROXY_TRUSTED_HOSTS comma-separated host/IP/CIDR list - Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header
attachment-cache-dir NTFY_ATTACHMENT_CACHE_DIR directory or S3 URL - Cache directory for attached files, or S3 URL for object storage (format: s3://KEY:SECRET@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]).
attachment-total-size-limit NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT size 5G Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected.
attachment-file-size-limit NTFY_ATTACHMENT_FILE_SIZE_LIMIT size 15M Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected.
attachment-expiry-duration NTFY_ATTACHMENT_EXPIRY_DURATION duration 3h Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects visitor-attachment-total-size-limit.
smtp-sender-addr NTFY_SMTP_SENDER_ADDR host:port - SMTP server address to allow email sending
smtp-sender-user NTFY_SMTP_SENDER_USER string - SMTP user; only used if e-mail sending is enabled
smtp-sender-pass NTFY_SMTP_SENDER_PASS string - SMTP password; only used if e-mail sending is enabled
smtp-sender-from NTFY_SMTP_SENDER_FROM e-mail address - SMTP sender e-mail address; only used if e-mail sending is enabled
smtp-sender-verify NTFY_SMTP_SENDER_VERIFY bool false If true, require verified email addresses for email notifications; anonymous email sending is disabled
smtp-server-listen NTFY_SMTP_SERVER_LISTEN [ip]:port - Defines the IP address and port the SMTP server will listen on, e.g. :25 or 1.2.3.4:25
smtp-server-domain NTFY_SMTP_SERVER_DOMAIN domain name - SMTP server e-mail domain, e.g. ntfy.sh
smtp-server-addr-prefix NTFY_SMTP_SERVER_ADDR_PREFIX string - Optional prefix for the e-mail addresses to prevent spam, e.g. ntfy-
twilio-account NTFY_TWILIO_ACCOUNT string - Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
twilio-auth-token NTFY_TWILIO_AUTH_TOKEN string - Twilio auth token, e.g. affebeef258625862586258625862586
twilio-phone-number NTFY_TWILIO_PHONE_NUMBER string - Twilio outgoing phone number, e.g. +18775132586
twilio-verify-service NTFY_TWILIO_VERIFY_SERVICE string - Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
keepalive-interval NTFY_KEEPALIVE_INTERVAL duration 45s Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that.
manager-interval NTFY_MANAGER_INTERVAL duration 1m Interval in which the manager prunes old messages, deletes topics and prints the stats.
message-size-limit NTFY_MESSAGE_SIZE_LIMIT size 4K The size limit for the message body. Please note that this is largely untested, and that FCM/APNS have limits around 4KB. If you increase this size limit, FCM and APNS will NOT work for large messages.
message-delay-limit NTFY_MESSAGE_DELAY_LIMIT duration 3d Amount of time a message can be scheduled into the future when using the Delay header
global-topic-limit NTFY_GLOBAL_TOPIC_LIMIT number 15,000 Rate limiting: Total number of topics before the server rejects new topics.
upstream-base-url NTFY_UPSTREAM_BASE_URL URL https://ntfy.sh Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers
upstream-access-token NTFY_UPSTREAM_ACCESS_TOKEN string tk_zyYLYj... Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth
visitor-attachment-total-size-limit NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT size 100M Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See attachment-expiry-duration.
visitor-attachment-daily-bandwidth-limit NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT size 500M Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding.
visitor-email-limit-burst NTFY_VISITOR_EMAIL_LIMIT_BURST number 16 Rate limiting:Initial limit of e-mails per visitor
visitor-email-limit-replenish NTFY_VISITOR_EMAIL_LIMIT_REPLENISH duration 1h Rate limiting: Strongly related to visitor-email-limit-burst: The rate at which the bucket is refilled
visitor-message-daily-limit NTFY_VISITOR_MESSAGE_DAILY_LIMIT number - Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset.
visitor-request-limit-burst NTFY_VISITOR_REQUEST_LIMIT_BURST number 60 Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has
visitor-request-limit-replenish NTFY_VISITOR_REQUEST_LIMIT_REPLENISH duration 5s Rate limiting: Strongly related to visitor-request-limit-burst: The rate at which the bucket is refilled
visitor-request-limit-exempt-hosts NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS comma-separated host/IP/CIDR list - Rate limiting: List of hostnames and IPs to be exempt from request rate limiting
visitor-subscription-limit NTFY_VISITOR_SUBSCRIPTION_LIMIT number 30 Rate limiting: Number of subscriptions per visitor (IP address)
visitor-subscriber-rate-limiting NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING bool false Rate limiting: Enables subscriber-based rate limiting
visitor-topic-creation-limit-burst NTFY_VISITOR_TOPIC_CREATION_LIMIT_BURST number 100 Rate limiting: Initial bucket of new topic creations per visitor. 0 disables the limit.
visitor-topic-creation-limit-replenish NTFY_VISITOR_TOPIC_CREATION_LIMIT_REPLENISH duration 1m Rate limiting: Rate at which the per-visitor topic-creation bucket is refilled (one new topic per x).
visitor-prefix-bits-ipv4 NTFY_VISITOR_PREFIX_BITS_IPV4 number 32 Rate limiting: Number of bits to use for IPv4 visitor prefix, e.g. 24 for /24
visitor-prefix-bits-ipv6 NTFY_VISITOR_PREFIX_BITS_IPV6 number 64 Rate limiting: Number of bits to use for IPv6 visitor prefix, e.g. 48 for /48
ban-file NTFY_BAN_FILE filename - Abuse ban-feed: file confirmed abusive visitor IPs are appended to, for fail2ban to tail. Empty disables the feature. See Banning bad actors
ban-window NTFY_BAN_WINDOW duration 10m Abuse ban-feed: rolling window over which weighted strikes are counted, per IP prefix
ban-threshold NTFY_BAN_THRESHOLD number 100 Abuse ban-feed: weighted strikes per ban-window before a prefix is written to ban-file
ban-weights NTFY_BAN_WEIGHTS list of KEY:WEIGHT 42909:10 Abuse ban-feed: per-code strike weights (exact code, family 429*, or *; longest match wins; 0 exempts). See Banning bad actors
web-root NTFY_WEB_ROOT path, e.g. / or /app, or disable / Sets root of the web app (e.g. /, or /app), or disables it entirely (disable)
enable-signup NTFY_ENABLE_SIGNUP boolean (true or false) false Allows users to sign up via the web app, or API
enable-login NTFY_ENABLE_LOGIN boolean (true or false) false Allows users to log in via the web app, or API
enable-reservations NTFY_ENABLE_RESERVATIONS boolean (true or false) false Allows users to reserve topics (if their tier allows it)
require-login NTFY_REQUIRE_LOGIN boolean (true or false) false All actions via the web app require a login
stripe-secret-key NTFY_STRIPE_SECRET_KEY string - Payments: Key used for the Stripe API communication, this enables payments
stripe-webhook-key NTFY_STRIPE_WEBHOOK_KEY string - Payments: Key required to validate the authenticity of incoming webhooks from Stripe
billing-contact NTFY_BILLING_CONTACT email address or website - Payments: Email or website displayed in Upgrade dialog as a billing contact
web-push-public-key NTFY_WEB_PUSH_PUBLIC_KEY string - Web Push: Public Key. Run ntfy webpush keys to generate
web-push-private-key NTFY_WEB_PUSH_PRIVATE_KEY string - Web Push: Private Key. Run ntfy webpush keys to generate
web-push-file NTFY_WEB_PUSH_FILE string - Web Push: Database file that stores subscriptions
web-push-email-address NTFY_WEB_PUSH_EMAIL_ADDRESS string - Web Push: Sender email address
web-push-startup-queries NTFY_WEB_PUSH_STARTUP_QUERIES string - Web Push: SQL queries to run against subscription database at startup
web-push-expiry-duration NTFY_WEB_PUSH_EXPIRY_DURATION duration 60d Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions.
web-push-expiry-warning-duration NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION duration 55d Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions.
log-format NTFY_LOG_FORMAT string text Defines the output format, can be text or json
log-file NTFY_LOG_FILE string - Defines the filename to write logs to. If this is not set, ntfy logs to stderr
log-level NTFY_LOG_LEVEL string info Defines the default log level, can be one of trace, debug, info, warn or error

The format for a duration is: <number>(smhd), e.g. 30s, 20m, 1h or 3d.
The format for a size is: <number>(GMK), e.g. 1G, 200M or 4000k.

Command line options

text
/* Detailed source-code truncated for AI context efficiency. */

File: docs/deprecations.md

Deprecations and breaking changes

This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
removed after 1-3 months from the time they were deprecated. How long the feature is deprecated
before the behavior is changed depends on the severity of the change, and how prominent the feature is.

Active deprecations

No active deprecations

Previous deprecations

ntfy CLI: `ntfy publish --env-topic` will be removed

Active since 2022-06-20, behavior changed with v1.30.1

The ntfy publish --env-topic option will be removed. It'll still be possible to specify a topic via the
NTFY_TOPIC environment variable, but it won't be necessary anymore to specify the --env-topic flag.

=== "Before"
$ NTFY_TOPIC=mytopic ntfy publish --env-topic "this is the message"

=== "After"
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"

Android app: WebSockets will become the default connection protocol

Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)

Instant delivery connections and connections to self-hosted servers in the Android app were going to switch
to use the WebSockets protocol by default. It was decided to keep JSON stream as the most compatible default
and add a notice banner in the Android app instead.

Android app: Using `since=` instead of `since=`

Active since 2022-02-27, behavior changed with v1.14.0

The Android app started using since= instead of since=<timestamp>, which means as of Android app v1.14.0,
it will not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app.

The since=<timestamp> endpoint will continue to work. This is merely a notice that the Android app behavior will change.

Running server via `ntfy` (instead of `ntfy serve`)

Deprecated 2021-12-17, behavior changed with v1.10.0

As more commands are added to the ntfy CLI tool, using just ntfy to run the server is not practical
anymore. Please use ntfy serve instead. This also applies to Docker images, as they can also execute more than
just the server.

=== "Before"
$ ntfy 2021/12/17 08:16:01 Listening on :80/http

=== "After"
$ ntfy serve 2021/12/17 08:16:01 Listening on :80/http


File: docs/develop.md

Development

Hurray ๐Ÿฅณ ๐ŸŽ‰, you are interested in writing code for ntfy! That's awesome. ๐Ÿ˜Ž

I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't
hesitate to contact me on Discord or Matrix.

ntfy server

The ntfy server source code is available on GitHub. The codebase for the
server consists of three components:

  • The main server/client is written in Go (so you'll need Go). Its main entrypoint is at
    main.go, and the meat you're likely interested in is
    in server.go. Notably, the server uses a
    SQLite library called go-sqlite3, which requires
    Cgo and CGO_ENABLED=1 to be set. Otherwise things will not work (see below).
  • The documentation is generated by MkDocs and Material for MkDocs,
    which is written in Python. You'll need Python and MkDocs (via pip) only if you want to
    build the docs.
  • The web app is written in React, using MUI. It uses Create React App
    to build the production build. If you want to modify the web app, you need nodejs (for npm)
    and install all the 100,000 dependencies (sigh).

All of these components are built and then baked into one binary.

Code:

  • main.go - Main entrypoint into the CLI, for both server and client
  • cmd/ - CLI commands, such as serve or publish
  • server/ - The meat of the server logic
  • docs/ - The MkDocs documentation, also see mkdocs.yml
  • web/ - The React application, also see web/package.json

Build related:

The web/ and docs/ folder are the sources for web app and documentation. During the build process,
the generated output is copied to server/site (web app and landing page) and server/docs (documentation).

Build requirements

  • Go (required for main server)
  • gcc (required main server, for SQLite cgo-based bindings)
  • Make (required for convenience)
  • libsqlite3/libsqlite3-dev (required for main server, for SQLite cgo-based bindings)
  • GoReleaser (required for a proper main server build)
  • Python (for pip, only to build the docs)
  • nodejs (for npm, only to build the web app)

Install dependencies

These steps assume Ubuntu. Steps may vary on different Linux distributions.

First, install Go (see official instructions):

shell
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version   # verifies that it worked

Install GoReleaser (see official instructions):

shell
go install github.com/goreleaser/goreleaser@latest
goreleaser -v   # verifies that it worked

Install nodejs (see official instructions):

shell
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v   # verifies that it worked

Then install a few other things required:

shell
sudo apt install \
    build-essential \
    libsqlite3-dev \
    gcc-arm-linux-gnueabi \
    gcc-aarch64-linux-gnu \
    python3-pip \
    upx \
    git

Check out code

Now check out via git from the GitHub repository:

=== "via HTTPS"
shell git clone https://github.com/binwiederhier/ntfy.git cd ntfy

=== "via SSH"
shell git clone [email protected]:binwiederhier/ntfy.git cd ntfy

Build all the things

Now you can finally build everything. There are tons of make targets, so maybe just review what's there first
by typing make:

shell
$ make 
Typical commands (more see below):
  make build                   - Build web app, documentation and server/client (sloowwww)
  make cli-linux-amd64         - Build server/client binary (amd64, no web app or docs)
  make install-linux-amd64     - Install ntfy binary to /usr/bin/ntfy (amd64)
  make web                     - Build the web app
  make docs                    - Build the documentation
  make check                   - Run all tests, vetting/formatting checks and linters
...

If you want to build the ntfy binary including web app and docs for all supported architectures (amd64, armv7, and arm64),
you can simply run make build:

shell
$ make build
...
# This builds web app, docs, and the ntfy binary (for amd64, armv7 and arm64). 
# This will be SLOW (5+ minutes on my laptop on the first run). Maybe look at the other make targets?

You'll see all the outputs in the dist/ folder afterwards:

bash
$ find dist 
dist
dist/metadata.json
dist/ntfy_arm64_linux_arm64
dist/ntfy_arm64_linux_arm64/ntfy
dist/ntfy_armv7_linux_arm_7
dist/ntfy_armv7_linux_arm_7/ntfy
dist/ntfy_amd64_linux_amd64
dist/ntfy_amd64_linux_amd64/ntfy
dist/config.yaml
dist/artifacts.json

If you also want to build the Debian/RPM packages and the Docker images for all supported architectures, you can
use the make release-snapshot target:

shell
$ make release-snapshot
...
# This will be REALLY SLOW (sometimes 5+ minutes on my laptop)

During development, you may want to be more picky and build only certain things. Here are a few examples.

Build the ntfy binary

To build only the ntfy binary without the web app or documentation, use the make cli-... targets:

shell
$ make
Build server & client (not release version):
  make cli                     - Build server & client (all architectures)
  make cli-linux-amd64         - Build server & client (Linux, amd64 only)
  make cli-linux-armv6         - Build server & client (Linux, armv6 only)
  make cli-linux-armv7         - Build server & client (Linux, armv7 only)
  make cli-linux-arm64         - Build server & client (Linux, arm64 only)
  make cli-windows-amd64       - Build client (Windows, amd64 only)

So if you're on an amd64/x86_64-based machine, you may just want to run make cli-linux-amd64 during testing. On a modern
system, this shouldn't take longer than 5-10 seconds. I often combine it with install-linux-amd64 so I can run the binary
right away:

shell
$ make cli-linux-amd64 install-linux-amd64
$ ntfy serve

During development of the main app, you can also just use go run main.go, as long as you run
make cli-deps-static-sitesat least once and CGO_ENABLED=1:

shell
$ export CGO_ENABLED=1
$ make cli-deps-static-sites
$ go run main.go serve
2022/03/18 08:43:55 Listening on :2586[http]
...

If you don't run cli-deps-static-sites, you may see an error pattern ...: no matching files found:

text
$ go run main.go serve
server/server.go:85:13: pattern docs: no matching files found

This is because we use go:embed to embed the documentation and web app, so the Go code expects files to be
present at server/docs and server/site. If they are not, you'll see the above error. The cli-deps-static-sites
target creates dummy files that ensures that you'll be able to build.

Build the web app

The sources for the web app live in web/. As long as you have npm installed (see above), building the web app
is really simple. Just type make web and you're in business:

shell
$ make web
...

This will build the web app using Create React App and then copy the production build to the server/site folder, so
that when you make cli (or make cli-linux-amd64, ...), you will have the web app included in the ntfy binary.

If you're developing on the web app, it's best to just cd web and run npm start manually. This will open your browser
at http://127.0.0.1:3000 with the web app, and as you edit the source files, they will be recompiled and the browser
will automatically refresh:

shell
$ cd web
$ npm start

Build the docs

The sources for the docs live in docs/. Similarly to the web app, you can simply run make docs to build the
documentation. As long as you have mkdocs installed (see above), this should work fine:

shell
$ make docs
...

If you are changing the documentation, you should be running mkdocs serve directly. This will build the documentation,
serve the files at http://127.0.0.1:8000/, and rebuild every time you save the source files:

text
$ mkdocs serve
INFO     -  Building documentation...
INFO     -  Cleaning site directory
INFO     -  Documentation built in 5.53 seconds
INFO     -  [16:28:14] Serving on http://127.0.0.1:8000/

Then you can navigate to http://127.0.0.1:8000/ and whenever you change a markdown file in your text editor it'll automatically update.

Android app

The ntfy Android app source code is available on GitHub.
The Android app has two flavors:

  • Google Play: The play flavor includes Firebase (FCM) and requires a Firebase account
  • F-Droid: The fdroid flavor does not include Firebase or Google dependencies
  • main/ - Main Android app source code
  • play/ - Google Play / Firebase specific code
  • fdroid/ - F-Droid Firebase stubs
  • build.gradle - Main build file

IDE/Environment

You should download Android Studio (or IntelliJ IDEA
with the relevant Android plugins). Everything else will just be a pain for you. Do yourself a favor. ๐Ÿ˜€

Check out the code

First check out the repository:

=== "via HTTPS"
shell git clone https://github.com/binwiederhier/ntfy-android.git cd ntfy-android

=== "via SSH"
shell git clone [email protected]:binwiederhier/ntfy-android.git cd ntfy-android

Then either follow the steps for building with or without Firebase.

Build F-Droid flavor (no FCM)

!!! info
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.

Without Firebase, you may want to still change the default app_base_url in strings.xml
if you're self-hosting the server. Then run:

text
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease

# To build a bundle .aab (app/fdroid/release/*.aab)
./gradlew bundleFdroidRelease

Build Play flavor (FCM)

!!! info
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.

To build your own version with Firebase, you must:

  • Create a Firebase/FCM account
  • Place your account file at app/google-services.json
  • And change app_base_url in strings.xml
  • Then run:
text
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease

# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease