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:
- Discord server - direct chat with the community
- Matrix room #ntfy (+ Matrix space) - same chat, bridged from Discord
- GitHub issues - questions, features, bugs
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:
- github.com/urfave/cli (MIT) is used to drive the CLI
- Mixkit sounds (Mixkit Free License) are used as notification sounds
- Sounds from notificationsounds.com (Creative Commons Attribution) are used as notification sounds
- Roboto Font (Apache 2.0) is used as a font in everything web
- React (MIT) is used for the web app
- Material UI components (MIT) are used in the web app
- MUI dashboard template (MIT) was used as a basis for the web app
- Dexie.js (Apache 2.0) is used for web app persistence in IndexedDB
- GoReleaser (MIT) is used to create releases
- go-smtp (MIT) is used to receive e-mails
- stretchr/testify (MIT) is used for unit and integration tests
- github.com/mattn/go-sqlite3 (MIT) is used to provide the persistent message cache
- Firebase Admin SDK (Apache 2.0) is used to send FCM messages
- github/gemoji (MIT) is used for emoji support (specifically the emoji.json file)
- Go's text/template (BSD-3-Clause) is vendored under template/gotext/ with a small patch adding an execution deadline (see template/gotext/README.md)
- Lightbox with vanilla JS as a lightbox on the landing page
- HTTP middleware for gzip compression (MIT) is used for serving static files
- Regex for auto-linking (MIT) is used to highlight links (the library is not used)
- Statically linking go-sqlite3
- Linked tabs in mkdocs
- webpush-go (MIT) is used to send web push notifications
- Sprig (MIT) is used to add template parsing functions
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:
- JSON stream:
<topic>/jsonreturns a JSON stream, with one JSON message object per line - SSE stream:
<topic>/ssereturns messages as Server-Sent Events (SSE), which
can be used with EventSource - Raw stream:
<topic>/rawreturns messages as raw text, with one line per message
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
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"}
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
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
Disk full
...
```=== "HTTP"
``` http
GET /disk-alerts/raw HTTP/1.1
Host: ntfy.sh
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
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).
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).
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 withsince=, you can use since=latest to grab the most recent message from the cache for a particular topic.
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):
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 andtags. 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.
$ 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:
$ 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
authquery 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:
# 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.comPublish messages
You can send messages with the ntfy CLI using the ntfy publish command (or any of its aliases pub, send ortrigger). 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
ntfy subscribe TOPICIf 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.
$ 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!"}
...Run command for every message
ntfy subscribe TOPIC COMMANDIf 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:
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'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
ntfy subscribe --from-configTo 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-thissimply echos to standard out - Messages to
alertsdisplay as desktop notification for high priority messages using notify-send (Linux),
notifu (Windows) orosascript(macOS) - Messages to
calcopen the calculator ๐ (because, why not) - Messages to
print-tempexecute 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:
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:
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-clientAuthentication
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:
ntfy subscribe \
-u phil:mypass \
ntfy.example.com/mysecretsFile: 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.
[](../../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:
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:
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:
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:
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:
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:
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:
[](../../static/img/android-screenshot-share-1.jpg)
[](../../static/img/android-screenshot-share-2.jpg)ntfy:// links
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:
[](../../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:
[](../../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:
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:
[](../../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:
[](../../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.
[](../../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.
[](../../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:
[](../../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":
[](../../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.
[](../../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:
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:
$ ntfy serve
2021/11/30 19:59:08 Listening on :80You 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
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
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 thedatabase-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 settingauth-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 is12h).
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 thesince= 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-urlis the root URL for the ntfy server; this is needed for the generated attachment URLsattachment-cache-diris the cache directory for attached files, or an S3 URL for object storageattachment-total-size-limitis the size limit of the attachment storage (default: 5G)attachment-file-size-limitis the per-file attachment size limit (e.g. 300k, 2M, 100M, default: 15M)attachment-expiry-durationis 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:
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:
{
"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-fileis 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, ifdatabase-urlis set,
auth is automatically enabled using PostgreSQL (see database options).auth-default-accessdefines the default/fallback access if no access control entry is found; it can be
set toread-write(default),read-only,write-onlyordeny-all. If you are setting up a private instance,
you'll want to set this todeny-all(see private instance example).
Once configured, you can use
- the
ntfy usercommand and theauth-usersconfig option to add or modify users - the
ntfy accesscommand and theauth-accessoption to modify the access control list
and topic patterns, and - the
ntfy tokencommand and theauth-tokensconfig option to manage access tokens for users.
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 usercommand, you can manually add/update/remove users. - In the config: You can provision users in the
server.ymlfile viaauth-userskey.
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 usingntfy 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):
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 optionUsers 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.
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 accesscommand, you can manually edit the access control list. - In the config: You can provision ACL entries in the
server.ymlfile viaauth-accesskey.
ACL entries via the CLI
The ACL can be displayed or modified with the ntfy access command:
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 TOPICA 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 messagesread-only(aliases:read,ro): Allows only subscribing and reading messages, but not publishing to the topicwrite-only(aliases:write,wo): Allows only publishing to the topic, but not subscribing to itdeny(alias:none): Allows neither publishing nor subscribing to a topic
Example commands (type ntfy access --help for more details):
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 mytopicExample ACL:
$ 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-writeorrw: Allows both publishing to and subscribing to the topicread-only,read, orro: Allows only subscribing to the topicwrite-only,write, orwo: Allows only publishing to the topicdeny-all,deny, ornone: 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 tokencommand, you can manually add/update/remove tokens. - In the config: You can provision access tokens in the
server.ymlfile viaauth-tokenskey.
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):
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 optionCreating an access token:
$ 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 ESTOnce 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=
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-urlis the root URL for the ntfy server; this is needed for e-mail footersmtp-sender-addris the hostname:port of the SMTP serversmtp-sender-userandsmtp-sender-passare the username and password of the SMTP usersmtp-sender-fromis the e-mail address of the sendersmtp-sender-verifyis a flag that forces email recipient verification when enabled. If set to true,
only verified email recipients can be used in theX-Emailheader.
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-listendefines the IP address and port the SMTP server will listen on, e.g.:25or1.2.3.4:25smtp-server-domainis the e-mail domain, e.g.ntfy.sh(must be identical to MX record, see below)smtp-server-addr-prefixis an optional prefix for the e-mail addresses to prevent spam. If set tontfy-, 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):
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
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.
$ 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 thisAs for the DNS setup, be sure to verify that dig MX and dig A are returning results similar to this:
$ dig MX ntfy.sh +short
10 mx1.ntfy.sh.
$ dig A mx1.ntfy.sh +short
3.139.215.220Local-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-proxymakes it so that the real visitor IP address is extracted from the header defined inproxy-forwarded-header.
Without this, the remote address of the incoming connection is used (default:false).proxy-forwarded-headeris 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-hostsis 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-ipv4is the number of bits of the IPv4 address to use for rate limiting (default is32, 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 thevisitor-prefix-bits-ipv4config option. To group visitors in a /24 subnet and count them as one, for instance,
set it to24. In that case,1.2.3.4and1.2.3.99are treated as the same visitor.visitor-prefix-bits-ipv6is the number of bits of the IPv6 address to use for rate limiting (default is64, which is a /64 subnet).
In IPv6 environments, by default, a visitor's IP address is truncated to the /64 subnet, meaning that2001:db8:25:86:1::1and2001:db8:25:86:2::1are treated as the same visitor. Use thevisitor-prefix-bits-ipv6config 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. */
=== "nginx (more secure)"
```
/* Detailed source-code truncated for AI context efficiency. */=== "Apache >= 2.4.47"
```
# /etc/apache2/sites-*/ntfy.conf
<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. */
=== "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"
## 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 protectedIf 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 tohttps://ntfy.sh/6de73be8dfb7d69e.... The request from your server to the upstream server
contains only the message ID (in theX-Poll-IDheader), 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:
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-keyis the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890web-push-private-keyis the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890web-push-fileis a database file to keep track of browser subscription endpoints, e.g./var/cache/ntfy/webpush.db(not required ifdatabase-urlis set)web-push-email-addressis the admin email address send to the push provider, e.g.[email protected]web-push-startup-queriesis an optional list of queries to run on startup`web-push-expiry-warning-durationdefines the duration after which unused subscriptions are sent a warning (default is55d)web-push-expiry-durationdefines the duration after which unused subscriptions will expire (default is60d)
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:
$ 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:
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):
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):
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 \
proPayments
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-keyis 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-keyis 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-contactis 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:
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-accountis the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586twilio-auth-tokenis the Twilio auth token, e.g. affebeef258625862586258625862586twilio-phone-numberis the outgoing phone number you purchased, e.g. +18775132586twilio-verify-serviceis the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586twilio-call-formatis 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: |
<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: |
<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-limitdefines 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-limitdefines 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 theX-Forwarded-Forheader ifbehind-proxyis set). All config options that start with the wordvisitorapply
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-limitdefines the total number of topics before the server rejects new topics. It defaults to 15,000.visitor-subscription-limitis 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-burstis the initial bucket of requests each visitor has. This defaults to 60.visitor-request-limit-replenishis the rate at which the bucket is refilled (one request per x). Defaults to 5s.visitor-request-limit-exempt-hostsis 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-limitis 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 viaX-Attach,
see publishing docs) do not count here.visitor-attachment-daily-bandwidth-limitis 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-burstis the initial bucket of emails each visitor has. This defaults to 16.visitor-email-limit-replenishis 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-burstis the initial bucket of new-topic tokens. Set to 0 to disable
the limit entirely. Defaults to 100.visitor-topic-creation-limit-replenishis 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:
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitorIPv6 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-ipv4is number of bits of the IPv4 address to use for rate limiting (default: 32, full address)visitor-prefix-bits-ipv6is 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'svisitor-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:
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 theLimitNOFILE 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;
}
# 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-fileis 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 andcopytruncate) so it does not grow unbounded.ban-windowis the rolling window over which weighted strikes are counted, per IP prefix.ban-thresholdis the number of weighted strikes perban-windowbefore a prefix is written toban-file. Each prefix has one shared budget, so it cannot be gamed by mixing error codes.ban-weightsassigns a strike weight per matcher key, formatted asKEY:WEIGHT. A key is an exact
ntfy error code (42909), a code family (429*,403*,4*), a bare HTTP status (403, short for403*), or*. The longest matching key wins. A weight of0exempts 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 weight1(i.e. it can be banned);
set*:0to 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:
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 42909is 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 thelisten-httpand/orlisten-httpsoptions in yourserver.ymlfile 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
/64subnet 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 thevisitor-prefix-bits-ipv6option in yourserver.ymlfile to a different
value (e.g.48for/48subnets). See IPv6 considerations and IP-based rate limiting for more details. - Banning IPs with fail2ban: By default, if you're using the
iptables-multiportaction, fail2ban bans individual IPv4 and IPv6 addresses viaiptablesandip6tables. 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 youractionbanandactionunbancommands
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.
{"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-metricsenables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)metrics-listen-httpmoves 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 ifenable-metricsis 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):
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-formatdefines the output format, can betext(default) orjsonlog-fileis a filename to write logs to. If this is not set, ntfy logs to stderr.log-leveldefines the default log level, can be one oftrace,debug,info(default),warnorerror.
Be aware thatdebug(and particularlytrace) can be very verbose. Only turn them on briefly for debugging purposes.log-level-overrideslets 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 -> levelto match a value exactly, e.g.tag=manager -> tracefield -> levelto match any value, e.g.time_taken_ms -> debug
Logging config (good for production use):
log-level: info
log-format: json
log-file: /var/log/ntfy.logTemporary 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:
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:
$ 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 TRACEConfig 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
/* 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 theNTFY_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 andCGO_ENABLED=1to 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 (viapip) 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 (fornpm)
and install all the 100,000 dependencies (sigh).
All of these components are built and then baked into one binary.
Navigating the code
Code:
- main.go - Main entrypoint into the CLI, for both server and client
- cmd/ - CLI commands, such as
serveorpublish - 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:
- Makefile - Main entrypoint for all things related to building
- .goreleaser.yml - Describes all build outputs (for GoReleaser)
- go.mod - Go modules dependency file
- mkdocs.yml - Config file for the docs (for MkDocs)
- web/package.json - Build and dependency file for web app (for npm)
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):
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 workedInstall GoReleaser (see official instructions):
go install github.com/goreleaser/goreleaser@latest
goreleaser -v # verifies that it workedInstall nodejs (see official instructions):
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v # verifies that it workedThen install a few other things required:
sudo apt install \
build-essential \
libsqlite3-dev \
gcc-arm-linux-gnueabi \
gcc-aarch64-linux-gnu \
python3-pip \
upx \
gitCheck 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:
$ 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:
$ 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:
$ 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.jsonIf 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:
$ 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:
$ 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:
$ make cli-linux-amd64 install-linux-amd64
$ ntfy serveDuring development of the main app, you can also just use go run main.go, as long as you runmake cli-deps-static-sitesat least once and CGO_ENABLED=1:
$ 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:
$ go run main.go serve
server/server.go:85:13: pattern docs: no matching files foundThis 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:
$ 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:
$ cd web
$ npm startBuild 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:
$ 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:
$ 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
playflavor includes Firebase (FCM) and requires a Firebase account - F-Droid: The
fdroidflavor does not include Firebase or Google dependencies
Navigating the code
- 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:
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
# To build a bundle .aab (app/fdroid/release/*.aab)
./gradlew bundleFdroidReleaseBuild 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_urlin strings.xml - Then run:
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease