## File: README.md

<hr>



# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[](https://github.com/binwiederhier/ntfy/releases/latest)
[](https://pkg.go.dev/heckel.io/ntfy/v2)
[](https://github.com/binwiederhier/ntfy/actions)
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[](https://codecov.io/gh/binwiederhier/ntfy)
[](https://discord.gg/cT7ECsZj9w)
[](https://matrix.to/#/#ntfy:matrix.org)
[](https://matrix.to/#/#ntfy-space:matrix.org)
[](https://ntfy.statuspage.io/)
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)

**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) 
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](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).


  [](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
  [](https://f-droid.org/en/packages/io.heckel.ntfy/)
  [](https://apps.apple.com/us/app/ntfy/id1625396347)



  
  
  
  
  


## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of 
ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
I would be very humbled by your sponsorship. ❤️ 

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

[Getting started](https://ntfy.sh/docs/) |
[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
[API](https://ntfy.sh/docs/publish/) |
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)

## 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](https://discord.gg/cT7ECsZj9w) - direct chat with the community
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs

## Announcements/beta testers
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) 
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). 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](https://github.com/sponsors/binwiederhier) or
and [Liberapay](https://liberapay.com/ntfy). 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:

[](https://m.do.co/c/442b929528db)

[](https://go.warp.dev/ntfy)

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

[](https://github.com/neutralinsomniac)
[](https://github.com/aspyct)
[](https://github.com/nickexyz)
[](https://github.com/qcasey)
[](https://github.com/mckay115)
[](https://github.com/Salamafet)
[](https://github.com/codinghipster)
[](https://github.com/HinFort)
[](https://github.com/Lexevolution)
[](https://github.com/johnnyip)
[](https://github.com/JonDerThan)
[](https://github.com/12nick12)
[](https://github.com/eanplatter)
[](https://github.com/fnoelscher)
[](https://github.com/bnorick)
[](https://github.com/snh)
[](https://github.com/hen-x)
[](https://github.com/JamieGoodson)
[](https://github.com/cremesk)
[](https://github.com/dangowans)
[](https://github.com/mnault)
[](https://github.com/nwithan8)
[](https://github.com/peterleiser)
[](https://github.com/portothree)
[](https://github.com/finngreig)
[](https://github.com/skrollme)
[](https://github.com/gergepalfi)
[](https://github.com/tonyakwei)
[](https://github.com/crosbyh)
[](https://github.com/mdlnr)
[](https://github.com/p-samuel)
[](https://github.com/zugaldia)
[](https://github.com/NathanSweet)
[](https://github.com/msdeibel)
[](https://github.com/ksurl)
[](https://github.com/CodingTimeDEV)
[](https://github.com/Terrormixer3000)
[](https://github.com/voroskoi)
[](https://github.com/Nickwasused)
[](https://github.com/bahur142)
[](https://github.com/vinhdizzo)
[](https://github.com/Ge0rg3)
[](https://github.com/biopsin)
[](https://github.com/thebino)
[](https://github.com/sky4055)
[](https://github.com/julianlam)
[](https://github.com/andreapx)
[](https://github.com/billycao)
[](https://github.com/zoic21)
[](https://github.com/IanKulin)
[](https://github.com/Joachim256)
[](https://github.com/overtone1000)
[](https://github.com/oakd)
[](https://github.com/KucharczykL)
[](https://github.com/hansbickhofe)
[](https://github.com/caseodilla)
[](https://github.com/0xAF)
[](https://github.com/soonoo)
[](https://github.com/nichu42)
[](https://github.com/samliebow)
[](https://github.com/johman10)
[](https://github.com/R-Gld)
[](https://github.com/FingerlessGlov3s)
[](https://github.com/Twisterado)
[](https://github.com/ScrumpyJack)
[](https://github.com/andrejarrell)
[](https://github.com/oaustegard)
[](https://github.com/CreativeWarlock)
[](https://github.com/darkdragon-001)
[](https://github.com/jonathan-kosgei)
[](https://github.com/KevinWang15)
[](https://github.com/darkmattercoder)
[](https://github.com/bmcgonag)
[](https://github.com/skorokithakis)
[](https://github.com/eenturk)
[](https://github.com/spirossi)
[](https://github.com/teomarcdhio)
[](https://github.com/MarcMichalsky)
[](https://github.com/LuckVintage)
[](https://github.com/spartan)
[](https://github.com/alexandzors)
[](https://github.com/dkramer95)
[](https://github.com/YezGotIt)
[](https://github.com/thomasskou)
[](https://github.com/surfernv)
[](https://github.com/richardleach)
[](https://github.com/bear)
[](https://github.com/cminter)
[](https://github.com/bahur142)
[](https://github.com/pgwiebes)
[](https://github.com/ralhei)
[](https://github.com/TechMDW)
[](https://github.com/ubipo)
[](https://github.com/tka85)
[](https://github.com/beekeeb)
[](https://github.com/Emiliaaah)
[](https://github.com/zark0s)
[](https://github.com/tomershvueli)
[](https://github.com/CataIana)
[](https://github.com/ajay-actuary)
[](https://github.com/mursec)
[](https://github.com/FrameXX)
[](https://github.com/vovayartsev)
[](https://github.com/dwain-lab)
[](https://github.com/brookmg)
[](https://github.com/siebej)
[](https://github.com/rxsantos)
[](https://github.com/hermannx5)
[](https://github.com/rwxd)
[](https://github.com/Integral-Tech)
[](https://github.com/TheTomik1)
[](https://github.com/dav23r)
[](https://github.com/stannynuytkens)
[](https://github.com/danbartram)
[](https://github.com/arthurgleckler)
[](https://github.com/tomroth04)
[](https://github.com/Circenn5130)
[](https://github.com/jceloria)
[](https://github.com/afunworm)
[](https://github.com/PTR-inc)
[](https://github.com/spudooli)
[](https://github.com/IMarkoMC)
[](https://github.com/rubund)
[](https://github.com/Riolku)
[](https://github.com/arnbrhm)
[](https://github.com/herzkerl)
[](https://github.com/0x45796164)
[](https://github.com/madchr1st)
[](https://github.com/avalentic)
[](https://github.com/TheCraiggers)
[](https://github.com/sheetd)
[](https://github.com/dlt-green)
[](https://github.com/suhlig)
[](https://github.com/Proximus888)
[](https://github.com/wielandp)
[](https://github.com/chxseh)
[](https://github.com/user8446)
[](https://github.com/cdf-eagles)

## 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](https://ntfy.sh/docs/develop/)
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).

<a href="https://hosted.weblate.org/engage/ntfy/">

</a>

## 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](CODE_OF_CONDUCT.md)._    

## License
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).   
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).

Third-party libraries and resources:
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
* [React](https://reactjs.org/) (MIT) is used for the web app
* [Material UI components](https://mui.com/) (MIT) are used in the web app
* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app
* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails
* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
* Go's [text/template](https://pkg.go.dev/text/template) (BSD-3-Clause) is vendored under [template/gotext/](template/gotext/) with a small patch adding an execution deadline (see [template/gotext/README.md](template/gotext/README.md))
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page 
* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications
* [Sprig](https://github.com/Masterminds/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](web.md), via the [phone app](phone.md), via the [ntfy CLI](cli.md),
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](../publish.md).

You can consume the subscription API as either a **[simple HTTP stream (JSON, SSE or raw)](#http-stream)**, or 
**[via WebSockets](#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](#subscribe-as-json-stream): `<topic>/json` returns a JSON stream, with one JSON message object per line
* [SSE stream](#subscribe-as-sse-stream): `<topic>/sse` returns messages as [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events), which
  can be used with [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
* [Raw stream](#subscribe-as-raw-stream): `<topic>/raw` returns 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](#subscribe-as-sse-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](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) in JavaScript, you can consume
notifications via a [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) 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](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-eventsource).

=== "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](../publish.md#message-priority), 
[tags](../publish.md#tags-emojis) or [message title](../publish.md#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](https://en.wikipedia.org/wiki/WebSocket), 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](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-websocket).
On the command line, I recommend [websocat](https://github.com/vi/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](#subscribe-as-json-stream). 

=== "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](../config.md#message-cache)) 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 with
`since=`, 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](../publish.md#scheduled-delivery) 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` and
`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags 
"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND. 

```
$ 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](../config.md#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](../publish.md#authentication), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
* or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw`

Please refer to the [publishing documentation](../publish.md#authentication) for additional details.

## JSON message format
Both the [`/json` endpoint](#subscribe-as-json-stream) and the [`/sse` endpoint](#subscribe-as-sse-stream) 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](../publish.md#updating-deleting-notifications)                                 |
| `message`     | -        | *string*                                                                        | `Some message`                                        | Message body; always present in `message` events                                                                                     |
| `title`       | -        | *string*                                                                        | `Some title`                                          | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>`                                               |
| `tags`        | -        | *string array*                                                                  | `["tag1","tag2"]`                                     | List of [tags](../publish.md#tags-emojis) that may or not map to emojis                                                              |
| `priority`    | -        | *1, 2, 3, 4, or 5*                                                              | `4`                                                   | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max                                                   |
| `click`       | -        | *URL*                                                                           | `https://example.com`                                 | Website opened when notification is [clicked](../publish.md#click-action)                                                            |
| `actions`     | -        | *JSON array*                                                                    | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#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](../publish.md#attachments) for details):

| Field     | Required | Type        | Example                        | Description                                                                                               |
|-----------|----------|-------------|--------------------------------|-----------------------------------------------------------------------------------------------------------|
| `name`    | ✔️       | *string*    | `attachment.jpg`               | Name of the attachment, can be overridden with `X-Filename`, see [attachments](../publish.md#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](web.md), the [phone app](phone.md), or the [API](api.md), 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](../install.md).

!!! info
    The **ntfy CLI is not required to send or receive messages**. You can instead [send messages with curl](../publish.md),
    and even use it to [subscribe to topics](api.md). 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](../install.md)**. 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](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub. 

If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
you may want to edit the `default-host` option:

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

## Publish messages
You can send messages with the ntfy CLI using the `ntfy publish` command (or any of its aliases `pub`, `send` or 
`trigger`). There are a lot of examples on the page about [publishing messages](../publish.md), 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 TOPIC
```
If you run the command like this, it prints the JSON representation of every incoming message. This is useful 
when you have a command that wants to stream-read incoming JSON messages. Unless `--poll` is passed, this command 
stays open forever.

```
$ 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!"}
...
```

<figure>
  <video controls muted autoplay loop width="650" src="../../static/img/cli-subscribe-video-1.mp4"></video>
  <figcaption>Subscribe in JSON mode</figcaption>
</figure>

### Run command for every message
```
ntfy subscribe TOPIC COMMAND
```
If you run it like this, a COMMAND is executed for every incoming messages. Scroll down to see a list of available
environment variables. Here are a few examples:
 
```
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'
```

<figure>
  <video controls muted autoplay loop width="650" src="../../static/img/cli-subscribe-video-2.webm"></video>
  <figcaption>Execute command on incoming messages</figcaption>
</figure>

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-config
```
To subscribe to multiple topics at once, and run different commands for each one, you can use `ntfy subscribe --from-config`,
which will read the `subscribe` config from the config file. Please also check out the [ntfy-client systemd service](#using-the-systemd-service).

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

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


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

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

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

* Messages to `echo-this` simply echos to standard out
* Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html) (Linux), 
  [notifu](https://www.paralint.com/projects/notifu/) (Windows) or `osascript` (macOS) 
* Messages to `calc` open the calculator 😀 (*because, why not*)
* Messages to `print-temp` execute an inline script and print the CPU temperature (Linux version only)

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

<figure>
  <video controls muted autoplay loop width="650" src="../../static/img/cli-subscribe-video-3.webm"></video>
  <figcaption>Execute all the things</figcaption>
</figure>

### Using the systemd service
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/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-client
```


### Authentication
Depending on whether the server is configured to support [access control](../config.md#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](https://en.wikipedia.org/wiki/Basic_access_authentication)
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/mysecrets
```

---

## File: docs/subscribe/phone.md

# Subscribe from your phone
You can use the [ntfy Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) to receive 
notifications directly on your phone. Just like the server, this app is also [open source](https://github.com/binwiederhier/ntfy-android).
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](https://github.com/binwiederhier/ntfy/issues/4).

[](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
[](https://f-droid.org/en/packages/io.heckel.ntfy/)

You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and 
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). 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](../publish.md) 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:

<figure>
  <video controls muted autoplay loop width="650" src="../../static/img/android-video-overview.mp4"></video>
  <figcaption>Sending push notifications to your Android phone</figcaption>
</figure>

## Message priority
When you [publish messages](../publish.md#message-priority) 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:

<figure markdown>
  { width=500 }
  <figcaption>High and urgent notifications show as pop-over</figcaption>
</figure>

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:

<figure markdown>
  { width=500 }
  <figcaption>Per-priority channels</figcaption>
</figure>

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:

<figure markdown>
  { width=500 }
  <figcaption>Per-priority sound/vibration settings</figcaption>
</figure>

## 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:

<figure markdown>
  { width=500 }
  <figcaption>Instant delivery foreground notification</figcaption>
</figure>

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:

<figure markdown>
  { width=500 }
  <figcaption>Turning off the persistent instant delivery notification</figcaption>
</figure>

**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)](https://firebase.google.com/docs/cloud-messaging). 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](#automation-apps)
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
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](https://github.com/binwiederhier/ntfy/issues/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](https://unifiedpush.org) is a standard for receiving push notifications without using the Google-owned
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) 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](https://unifiedpush.org/users/apps/). 
That's it. It's a one-step installation 😀. If desired, you can select your own [selfhosted ntfy server](../install.md)
to handle messages. Here's an example with [FluffyChat](https://fluffychat.im/):


    [](../../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](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm). 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](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt)).
Here's an example using [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
and [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm), 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:

<figure markdown>
  { width=500 }
  <figcaption>Muting notifications to prevent popups</figcaption>
</figure>

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](../publish.md#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](../publish.md#tags-emojis)                          |
| `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](../publish.md#message-priority) with 1=min, 3=default and 5=max |

#### Send messages using intents
To send messages from other apps (such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
and [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm)), 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](../publish.md). 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](../publish.md#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](../publish.md#tags-emojis)                          |
| `priority`   | -        | *String or Int (between 1-5)* | `4`               | Message [priority](../publish.md#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.

[](https://github.com/binwiederhier/ntfy/issues/4)

---

## 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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (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](https://caniuse.com/web-app-manifest) for details):

- **Chrome:** Android, Windows, Linux, macOS
- **Safari:** iOS 16.4+, macOS 14+
- **Firefox:** Android, as well as on Windows/Linux [via an extension](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/)
- **Edge:** Windows

Note that for self-hosted servers, [Web Push](../config.md#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](./web.md#background-notifications)
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](../publish.md).


    [](../../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:

<figure markdown>
  { width=500 }
  <figcaption>Pin web app to move it out of the way</figcaption>
</figure>

---

## 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](https://github.com/binwiederhier/ntfy/blob/main/server/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 :80
```

You can immediately start [publishing messages](publish.md), or subscribe via the [Android app](subscribe/phone.md),
[the web UI](subscribe/web.md), or simply via [curl or your favorite HTTP client](subscribe/api.md). To configure 
the server further, check out the [config options table](#config-options) or simply type `ntfy serve --help` to
get a list of [command line options](#command-line-options).

## Example config
!!! info
    Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/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](https://github.com/binwiederhier/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:mypassword@db.example.com: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: "ntfy@ntfy.sh"
    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


<figure markdown style="padding-left: 50px; padding-right: 50px; cursor: pointer;" onclick="document.getElementById('cg-open-btn').click();">
  
  <figcaption>The config generator helps you create a custom config for your self-hosted ntfy instance. Click to open.</figcaption>
</figure>






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
&times;




  Edit
  Preview



General
Database
Users
Message Cache
Attachments
Web Push
Email




<label>What URL will ntfy be reachable on?</label>



<label>Will ntfy run behind a proxy (e.g. nginx, Caddy)? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#behind-a-proxy-tls-etc)</label>

<label>No</label>
<label>Yes</label>



<label>Will this ntfy server be open or private? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#access-control)</label>

<label>Open</label>
<label>Private</label>
<label>Custom</label>



<label>Will iOS/iPhone users use this server? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#ios-instant-notifications)</label>

<label>No</label>
<label>Yes</label>



<label>Do you want to use ntfy as a UnifiedPush distributor? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#example-unifiedpush)</label>

<label>No</label>
<label>Yes</label>



<label>Which features do you want to enable?</label>

<label> User management and access control</label>Configure
<label> Persistent message cache</label>Configure
<label> Attachments</label>Configure
<label> Web push</label>Configure
<label> Email notifications</label>Configure
<label> Email publishing</label>Configure



<label>Which database backend would you like to use? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#database-options)</label>

<label>SQLite</label>
<label>PostgreSQL</label>




Configure user management, access control, and provisioned users/ACLs. See [access control](/config/#access-control) for details.

<label>Where should the user database be stored?</label>



<label>What should the default access policy be? [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#access-control)</label>
<select id="cg-default-access-select">
<option value="read-write" selected>Read &amp; Write</option>
<option value="read-only">Read Only</option>
<option value="write-only">Write Only</option>
<option value="deny-all">Deny All</option>
</select>


<label>Should login to the web app be enabled?</label>

<label>Disabled</label>
<label>Enabled</label>
<label>Required</label>



<label>Should it be possible to sign up via the web app?</label>

<label>No</label>
<label>Yes</label>







<label>Provisioned users [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#users-and-roles)</label>

+ Add user


<label>Provisioned ACLs [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#access-control-list-acl)</label>

+ Add ACL


<label>Provisioned tokens [<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"></svg>](/config/#access-tokens)</label>

+ Add token



Configure the message cache to allow devices to retrieve missed notifications. See [message cache](/config/#message-cache) for details.

<label>Where should the cache be stored?</label>



<label>How long should messages be cached?</label>




Allow users to upload and attach files to notifications. See [attachments](/config/#attachments) for details.

<label>Where should attachments be stored?</label>



<label>Max file size per attachment?</label>



<label>Total attachment storage limit?</label>



<label>How long before attachments expire?</label>




Enable browser push notifications via the Web Push API. VAPID keys are generated automatically. See [web push](/config/#web-push) for details.

<label>Where should web push data be stored?</label>



<label>Contact email address</label>



<label>Private key</label>



<label>Public key</label>



<label></label>
Regenerate keys



Configure outgoing email notifications and/or incoming email publishing. See [email notifications](/config/#e-mail-notifications) and [email publishing](/config/#e-mail-publishing) for details.

<label>**Outgoing (notifications)**</label>

<label>SMTP server address</label>



<label>Sender email</label>



<label>SMTP username</label>



<label>SMTP password</label>



<label>Require email verification</label>

<label>No</label>
<label>Yes</label>





<label>**Incoming (publishing)**</label>

<label>Listen address</label>



<label>Domain</label>



<label>Address prefix</label>





Configure the PostgreSQL connection. See [PostgreSQL](/config/#postgresql) for details.

<label>Database URL</label>









server.yml
docker-compose.yml
Env variables
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect></svg>


<code id="cg-code">Configure options on the left to generate your config...</code>







## Database options
ntfy uses a database for storing messages ([message cache](#message-cache)), users and [access control](#access-control), and [web push](#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](#message-cache).
* `auth-file`: Database file for authentication and [access control](#access-control). If set, enables auth.
* `web-push-file`: Database file for [web push](#web-push) subscriptions.

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

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

Note that setting `database-url` implicitly enables authentication and access control (equivalent to setting
`auth-file` with SQLite). The default access is `read-write`, so anonymous users can still read and write to all
topics. To restrict access, set `auth-default-access` to `deny-all` (see [access control](#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](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)
as query parameters, such as `sslmode`, `connect_timeout`, `sslcert`, `sslkey`, `sslrootcert`, and `application_name`.
See the [pgx driver documentation](https://pkg.go.dev/github.com/jackc/pgx/v5) 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](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
notifications that they may have missed. 

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

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

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

Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#poll-for-messages), as well as the
[`since=` parameter](subscribe/api.md#fetch-cached-messages).

## Attachments
If desired, you may allow users to upload and [attach files to notifications](publish.md#attachments). 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](#filesystem-storage) or in an [S3-compatible object store](#s3-storage), 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](https://ntfy.sh/app) via the web app.

The following config options are relevant to attachments:

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

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

Please also refer to the [rate limiting](#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](https://aws.amazon.com/s3/),
[DigitalOcean Spaces](https://www.digitalocean.com/products/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](https://m.do.co/c/442b929528db), you can get $200 credit**.

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

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

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

ntfy's auth implements two roles (`user` and `admin`) and per-topic `read` and `write` permissions using an 
[access control list (ACL)](https://en.wikipedia.org/wiki/Access-control_list). Access control entries can be applied 
to users as well as the special everyone user (`*`), which represents anonymous API access. 

To set up auth, **configure the following options**:

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

Once configured, you can use 

- the `ntfy user` command and the `auth-users` config option to [add or modify users](#users-and-roles)
- the `ntfy access` command and the `auth-access` option to [modify the access control list](#access-control-list-acl)
and topic patterns, and
- the `ntfy token` command and the `auth-tokens` config option to [manage access tokens](#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](#users-via-the-cli): Using the `ntfy user` command, you can manually add/update/remove users.
* [In the config](#users-via-the-config): You can provision users in the `server.yml` file via `auth-users` key.

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

**Roles:**

* Role `user` (default): Users with this role have no special permissions. Manage access using `ntfy access`
  (see [below](#access-control-list-acl)).
* 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 option
```

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

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

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

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

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

The password hash can be created using `ntfy user hash` or an [online bcrypt generator](https://bcrypt-generator.com/) (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](#acl-entries-via-the-cli): Using the `ntfy access` command, you can manually edit the access control list.
* [In the config](#acl-entries-via-the-config): You can provision ACL entries in the `server.yml` file via `auth-access` key.

#### ACL entries via the CLI
The ACL can be displayed or modified with the `ntfy access` command:

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

A `USERNAME` is an existing user, as created with `ntfy user add` (see [users and roles](#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](publish.md) to the given topic, as well as 
  [subscribing](subscribe/api.md) and reading messages
* `read-only` (aliases: `read`, `ro`): Allows only subscribing and reading messages, but not publishing to the topic
* `write-only` (aliases: `write`, `wo`): Allows only publishing to the topic, but not subscribing to it
* `deny` (alias: `none`): Allows neither publishing nor subscribing to a topic 

**Example commands** (type `ntfy access --help` for more details):
```
ntfy access                        # Shows entire access control list
ntfy access phil                   # Shows access for user phil
ntfy access phil mytopic rw        # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw    # Allow anonymous read-write access to mytopic
ntfy access everyone "up*" write   # Allow anonymous write-only access to topics "up..."
ntfy access --reset                # Reset entire access control list
ntfy access --reset phil           # Reset all access for user phil
ntfy access --reset phil mytopic   # Reset access for user phil and topic mytopic
```

**Example ACL:**
```
$ 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](#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](#users-via-the-config)),
or `everyone`/`*` for anonymous access. The `<topic-pattern>` can be a specific topic name or a pattern with wildcards (`*`). The 
`<access>` can be one of the following:

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

Here's an example with several ACL entries:

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

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

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

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

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

You can create access tokens in two different ways:

* [Using the CLI](#tokens-via-the-cli): Using the `ntfy token` command, you can manually add/update/remove tokens.
* [In the config](#tokens-via-the-config): You can provision access tokens in the `server.yml` file via `auth-tokens` key.

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

**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
```
ntfy token list                      # Shows list of tokens for all users
ntfy token list phil                 # Shows list of tokens for user phil
ntfy token add phil                  # Create token for user phil which never expires
ntfy token add --expires=2d phil     # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2sxr...  # Delete token
ntfy token generate                  # Generate random token, can be used in auth-tokens config option
```

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

Once an access token is created, you can **use it to authenticate against the ntfy server, e.g. when you publish or
subscribe to topics**. To learn how, check out [authenticate via access tokens](publish.md#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](#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](publish.md#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](#users-via-the-config)), 
access control entries in the `auth-access` section (see [ACL entries via the config](#acl-entries-via-the-config)),
and access tokens in the `auth-tokens` section (see [access tokens via the config](#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](https://en.wikipedia.org/wiki/Basic_access_authentication) 
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](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/developers/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …) 
has anonymous write access to the [topic](https://unifiedpush.org/developers/spec/definitions/#endpoint) used for push messages. 
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the 
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users-acl)** 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](publish.md#e-mail-notifications) (e.g. 
`curl -d "hi there" -H "X-Email: phil@example.com" 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](https://ntfy.sh/account). 

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

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

Here's an example config using [Amazon SES](https://aws.amazon.com/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: "ntfy@ntfy.sh"
    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](#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](publish.md#e-mail-publishing) (e.g. `mytopic@ntfy.sh` or 
`myprefix-mytopic@ntfy.sh`) to publish messages to a topic. This is useful for e-mail based integrations such as for 
statuspage.io (though these days most services also support webhooks and HTTP calls).

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

* `smtp-server-listen` defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25`
* `smtp-server-domain` is the e-mail domain, e.g. `ntfy.sh` (must be identical to MX record, see below)
* `smtp-server-addr-prefix` is an optional prefix for the e-mail addresses to prevent spam. If set to `ntfy-`, for instance,
  only e-mails to `ntfy-$topic@ntfy.sh` will be accepted. If this is not set, all emails to `$topic@ntfy.sh` 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](https://en.wikipedia.org/wiki/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](https://aws.amazon.com/route53/)):

<figure markdown>
  { width=600 }
  <figcaption>DNS records for incoming mail</figcaption>
</figure>

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: phil@example.com
RCPT TO: ntfy-mytopic@ntfy.sh
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 <ntfy-mytopic@ntfy.sh> gets this
```

As 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.220
```

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

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

Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); 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-alerts@example.com`. If the topic has access restrictions, you will need
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.

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](#rate-limiting) 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](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`)
as the primary identifier for a visitor, as opposed to the remote IP address. 

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

Relevant flags to consider:

* `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`.
  Without this, the remote address of the incoming connection is used (default: `false`).
* `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`),
  a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style
 header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`).
* `proxy-trusted-hosts` is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header 
  to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
  the forwarded header (default: empty).
* `visitor-prefix-bits-ipv4` is the number of bits of the IPv4 address to use for rate limiting (default is `32`, which is the entire
  IP address). In IPv4 environments, by default, a visitor's **full IPv4 address** is used as-is for rate limiting. This means that
  if someone publishes messages from multiple IP addresses, they will be counted as separate visitors. You can adjust this by setting the `visitor-prefix-bits-ipv4` config option. To group visitors in a /24 subnet and count them as one, for instance,
  set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor.
* `visitor-prefix-bits-ipv6` is the number of bits of the IPv6 address to use for rate limiting (default is `64`, which is a /64 subnet). 
  In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that `2001:db8:25:86:1::1` and 
  `2001:db8:25:86:2::1` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to adjust this behavior.
  See [IPv6 considerations](#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](#config-options). 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](https://certbot.eff.org/). I use it with the [dns-route53 plugin](https://certbot-dns-route53.readthedocs.io/en/stable/), 
which lets you use [AWS Route 53](https://aws.amazon.com/route53/) as the challenge. That's much easier than using the
HTTP challenge. I've found [this guide](https://nandovieira.com/using-lets-encrypt-in-development-with-nginx-and-aws-route53) 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 protected
```

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

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

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

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

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

```
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](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/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](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
a database to keep track of the browser's subscriptions, and an admin email address (you):

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

Alternatively, you can use PostgreSQL instead of SQLite by setting `database-url`
(see [PostgreSQL database](#postgresql)).

Limitations:

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

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

To configure VAPID keys, first generate them:

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

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

```yaml
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com
```

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](#payments),
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](#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 \
  pro
```

## Payments
ntfy supports paid [tiers](#tiers) via [Stripe](https://stripe.com/) 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](https://stripe.com/), set the `stripe-secret-key` and `stripe-webhook-key`
config options: 

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

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

Here's an example:

``` yaml
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
billing-contact: "phil@example.com"
```

## Phone calls
ntfy supports phone calls via [Twilio](https://www.twilio.com/) 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](publish.md#phone-calls) for more details.

To enable Twilio integration, sign up with [Twilio](https://www.twilio.com/), purchase a phone number (Toll free numbers
are the easiest), and then configure the following options:

* `twilio-account` is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
* `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
* `twilio-phone-number` is the outgoing phone number you purchased, e.g. +18775132586 
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
* `twilio-call-format` is the custom Twilio markup ([TwiML](https://www.twilio.com/docs/voice/twiml)) to use for phone calls (optional)

After you have configured phone calls, create a [tier](#tiers) 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](https://www.twilio.com/docs/voice/twiml). The format is
rendered as a [Go template](https://pkg.go.dev/text/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: |
      <Response>
        
        <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: |
      <Response>
        
        <Say loop="3" voice="alice" language="de-DE">
          Du hast eine Nachricht zum Thema {{.Topic}}.
          {{ if eq .Priority 5 }}
            Achtung. Die Nachricht ist sehr wichtig.
          {{ end }}
          
          {{ if neq .Title "" }}
            Titel der Nachricht: {{.Title}}.
          {{ end }}
          
          Nachricht:
          
          {{.Message}}
          
          Ende der Nachricht.
          
          Diese Nachricht wurde vom Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
          
        </Say>
        <Say voice="alice" language="de-DE">Alla mol!</Say>
      </Response>
    ```

## Message limits
There are a few message limits that you can configure:

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

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

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

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

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

### General limits
Let's do the easy limits first:

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

### Request limits
In addition to the limits above, there is a requests/second limit per visitor for all sensitive GET/PUT/POST requests.
This limit uses a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) (using Go's [rate package](https://pkg.go.dev/golang.org/x/time/rate)):

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

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

### Message limits
By default, the number of messages a visitor can send is governed entirely by the [request limit](#request-limits). 
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](#attachments)), there are two relevant 
per-visitor limits:

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

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

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

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

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

### Firebase limits
If [Firebase is configured](#firebase-fcm), all messages are also published to a Firebase topic (unless `Firebase: no` 
is set). Firebase enforces [its own limits](https://firebase.google.com/docs/cloud-messaging/concept-options#topics_throttling)
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 visitor
```

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

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

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

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

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

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

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

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

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

!!! info
    Due to a [denial-of-service issue](https://github.com/binwiederhier/ntfy/issues/1048), 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](https://www.reddit.com/r/golang/comments/r9u4ee/how_many_actively_connected_http_clients_can_a_go/).

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

### Message cache
By default, the [message cache](#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)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted. 
See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details.

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

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

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

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

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

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

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

### Proxy limits (nginx, Apache2)
If you are running [behind a proxy](#behind-a-proxy-tls-etc) (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](https://www.fail2ban.org/)
and nginx's [ngx_http_limit_req_module module](http://nginx.org/en/docs/http/ngx_http_limit_req_module.html) can be used
to ban client IPs if they misbehave. This is on top of the [rate limiting](#rate-limiting) inside the ntfy server.

Here's an example for how ntfy.sh is configured, following the instructions from two tutorials ([here](https://easyengine.io/tutorials/nginx/fail2ban/) 
and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-attack/)):

=== "/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](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban) for details. Ban actors are banned for 1 hour initially, and up to
4 hours at a time for repeated offenses. IPv4 addresses are banned individually, while IPv6 addresses are banned by their `/56` prefix.

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

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

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

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

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

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

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

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

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

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

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

!!! info
    The official ntfy.sh server supports IPv6. Check out ntfy.sh's [Ansible repository](https://github.com/binwiederhier/ntfy-ansible) for examples of how to
    configure [ntfy](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/ntfy), [nginx](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/nginx) and [fail2ban](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban).

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

```json
{"healthy":true}
```

See [Installation for Docker](install.md#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](https://prometheus.io/), which can then be used to
create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)).

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

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

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

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

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

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

Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on GitHub](https://raw.githubusercontent.com/binwiederhier/ntfy/main/examples/grafana-dashboard/ntfy-grafana.json)):

<figure markdown style="padding-left: 50px; padding-right: 50px">
  [](../../static/img/grafana-dashboard.png)
  <figcaption>ntfy Grafana dashboard</figcaption>
</figure>

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

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

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

The following config options define the logging behavior:

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

**Logging config (good for production use):**
``` yaml
log-level: info
log-format: json
log-file: /var/log/ntfy.log
```

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

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

Here's an example that will output only `info` log events, except when they match either of the defined overrides:
``` yaml
log-level: info
log-level-overrides:
  - "tag=manager -> trace"
  - "visitor_ip=1.2.3.4 -> debug"
  - "time_taken_ms -> debug"
```

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

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

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

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

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

| Config option                              | Env variable                                    | Format                                              | Default           | Description                                                                                                                                                                                                                             |
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `base-url`                                 | `NTFY_BASE_URL`                                 | *URL*                                               | -                 | Public facing base URL of the service (e.g. `https://ntfy.sh`)                                                                                                                                                                          |
| `listen-http`                              | `NTFY_LISTEN_HTTP`                              | `[host]:port`                                       | `:80`             | Listen address for the HTTP web server                                                                                                                                                                                                  |
| `listen-https`                             | `NTFY_LISTEN_HTTPS`                             | `[host]:port`                                       | -                 | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`.                                                                                                                                       |
| `listen-unix`                              | `NTFY_LISTEN_UNIX`                              | *filename*                                          | -                 | Path to a Unix socket to listen on                                                                                                                                                                                                      |
| `listen-unix-mode`                         | `NTFY_LISTEN_UNIX_MODE`                         | *file mode*                                         | *system default*  | File mode of the Unix socket, e.g. 0700 or 0777                                                                                                                                                                                         |
| `key-file`                                 | `NTFY_KEY_FILE`                                 | *filename*                                          | -                 | HTTPS/TLS private key file, only used if `listen-https` is set.                                                                                                                                                                         |
| `cert-file`                                | `NTFY_CERT_FILE`                                | *filename*                                          | -                 | HTTPS/TLS certificate file, only used if `listen-https` is set.                                                                                                                                                                         |
| `firebase-key-file`                        | `NTFY_FIREBASE_KEY_FILE`                        | *filename*                                          | -                 | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#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-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](#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](#message-cache)                                                                                                                           |
| `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](#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](publish.md#scheduled-delivery) 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](#banning-bad-actors-fail2ban)                                                             |
| `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](#banning-bad-actors-fail2ban)                                                                     |
| `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 the
`NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.

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

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

### <del>Android app: WebSockets will become the default connection protocol</del>
> 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=<timestamp>` 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](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)**.

## ntfy server
The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the
server consists of three components:

* **The main server/client** is written in [Go](https://go.dev/) (so you'll need Go). Its main entrypoint is at 
  [main.go](https://github.com/binwiederhier/ntfy/blob/main/main.go), and the meat you're likely interested in is 
  in [server.go](https://github.com/binwiederhier/ntfy/blob/main/server/server.go). Notably, the server uses a 
  [SQLite](https://sqlite.org) library called [go-sqlite3](https://github.com/mattn/go-sqlite3), which requires 
  [Cgo](https://go.dev/blog/cgo) and `CGO_ENABLED=1` to be set. Otherwise things will not work (see below).
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
  which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
  build the docs.
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
  to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`) 
  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](https://github.com/binwiederhier/ntfy/blob/main/main.go) - Main entrypoint into the CLI, for both server and client
* [cmd/](https://github.com/binwiederhier/ntfy/tree/main/cmd) - CLI commands, such as `serve` or `publish`
* [server/](https://github.com/binwiederhier/ntfy/tree/main/server) - The meat of the server logic
* [docs/](https://github.com/binwiederhier/ntfy/tree/main/docs) - The [MkDocs](https://www.mkdocs.org/) documentation, also see `mkdocs.yml`
* [web/](https://github.com/binwiederhier/ntfy/tree/main/web) - The [React](https://reactjs.org/) application, also see `web/package.json`

Build related:

* [Makefile](https://github.com/binwiederhier/ntfy/blob/main/Makefile) - Main entrypoint for all things related to building
* [.goreleaser.yml](https://github.com/binwiederhier/ntfy/blob/main/.goreleaser.yml) - Describes all build outputs (for [GoReleaser](https://goreleaser.com/))
* [go.mod](https://github.com/binwiederhier/ntfy/blob/main/go.mod) - Go modules dependency file
* [mkdocs.yml](https://github.com/binwiederhier/ntfy/blob/main/mkdocs.yml) - Config file for the docs (for [MkDocs](https://www.mkdocs.org/))
* [web/package.json](https://github.com/binwiederhier/ntfy/blob/main/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](https://go.dev/) (required for main server)
* [gcc](https://gcc.gnu.org/) (required main server, for SQLite cgo-based bindings)
* [Make](https://www.gnu.org/software/make/) (required for convenience)
* [libsqlite3/libsqlite3-dev](https://www.sqlite.org/) (required for main server, for SQLite cgo-based bindings)
* [GoReleaser](https://goreleaser.com/) (required for a proper main server build)
* [Python](https://www.python.org/) (for `pip`, only to build the docs) 
* [nodejs](https://nodejs.org/en/) (for `npm`, only to build the web app)

### Install dependencies
These steps **assume Ubuntu**. Steps may vary on different Linux distributions.

First, install [Go](https://go.dev/) (see [official instructions](https://go.dev/doc/install)):
``` shell
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version   # verifies that it worked
```

Install [GoReleaser](https://goreleaser.com/) (see [official instructions](https://goreleaser.com/install/)):
``` shell
go install github.com/goreleaser/goreleaser@latest
goreleaser -v   # verifies that it worked
```

Install [nodejs](https://nodejs.org/en/) (see [official instructions](https://nodejs.org/en/download/package-manager/)):
``` shell
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v   # verifies that it worked
```

Then install a few other things required:
``` shell
sudo apt install \
    build-essential \
    libsqlite3-dev \
    gcc-arm-linux-gnueabi \
    gcc-aarch64-linux-gnu \
    python3-pip \
    upx \
    git
```

### Check out code
Now check out via git from the [GitHub repository](https://github.com/binwiederhier/ntfy):

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

=== "via SSH"
    ``` shell
    git clone git@github.com:binwiederhier/ntfy.git 
    cd ntfy
    ```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

If you don't run `cli-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
```
$ go run main.go serve
server/server.go:85:13: pattern docs: no matching files found
```

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


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

``` shell
$ make web
...
```

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

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

``` shell
$ cd web
$ npm start
```

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

``` shell
$ make docs
...
```

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

```
$ 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](https://github.com/binwiederhier/ntfy-android).
The Android app has two flavors:

* **Google Play:** The `play` flavor includes [Firebase (FCM)](https://firebase.google.com/) and requires a Firebase account
* **F-Droid:** The `fdroid` flavor does not include Firebase or Google dependencies

### Navigating the code
* [main/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/main) - Main Android app source code
* [play/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/play) - Google Play / Firebase specific code
* [fdroid/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/fdroid) - F-Droid Firebase stubs
* [build.gradle](https://github.com/binwiederhier/ntfy-android/blob/main/app/build.gradle) - Main build file

### IDE/Environment
You should download [Android Studio](https://developer.android.com/studio) (or [IntelliJ IDEA](https://www.jetbrains.com/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 git@github.com: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](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/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 bundleFdroidRelease
```

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

To build your own version with Firebase, you must:

* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/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
```