A Rust implementation of the ntfy pub/sub notification server. Wire-compatible with existing ntfy clients (Android, iOS, web, CLI).
No cgo, no system SQLite dependency, no Firebase requirement. Single static binary.
- Publish messages via HTTP PUT/POST
- Subscribe via NDJSON stream, SSE, or WebSocket
- Multi-topic subscriptions (
/topic1,topic2/json) - Scheduled/delayed message delivery (
X-Delay,X-At,X-Inheaders) - SQLite message cache (bundled, no system dep)
- Optional authentication: Basic, Bearer token, ACL per topic
- User management API (self-service + admin)
- TLS via rustls + aws-lc-rs (no OpenSSL, no ring)
- Unix domain socket listener
- iOS upstream poll-forward
- UnifiedPush / Matrix Push Gateway relay
- Web Push notifications (VAPID + RFC 8188 AES-128-GCM, pure Rust — no OpenSSL)
Requires Rust 1.85 or later (rustup.rs). A C/C++ compiler is also required — aws-lc-sys compiles AWS-LC from source (TLS backend) and rusqlite compiles SQLite from source (message cache).
Install build tools:
| Distro | Command |
|---|---|
| Debian / Ubuntu | sudo apt install build-essential pkg-config |
| Fedora | sudo dnf install gcc gcc-c++ make pkgconf-pkg-config |
| Arch | sudo pacman -S base-devel |
| openSUSE | sudo zypper install -t pattern devel_basis |
Then build:
cargo build --release
# output: target/release/ntfy-rsThe first build takes a few minutes (SQLite and AWS-LC are compiled from source). Subsequent builds are fast.
xcode-select --install # installs clang and build tools (one-time)
cargo build --release
# output: target/release/ntfy-rs-
Install Rust from rustup.rs. Accept the defaults.
-
Install the C++ build tools — required to compile the bundled SQLite:
- Download Visual Studio Build Tools
- Select the "Desktop development with C++" workload and install
-
Open a new terminal (PowerShell or Command Prompt) and build:
cargo build --release
# output: target\release\ntfy-rs.exeThe first build takes a few minutes (SQLite is compiled from source). Subsequent builds are fast.
Troubleshooting
linker 'link.exe' not found— the C++ build tools are not on your PATH. Re-open the terminal after installing them, or use the "x64 Native Tools Command Prompt" shortcut installed with Visual Studio.cargo not found— close and reopen the terminal after installing rustup; it modifiesPATHbut the current session won't see the change.
Note: the Unix domain socket listener is disabled on Windows (
listen-unixhas no effect). All other features work normally.
Windows AV note: the release binary uses aws-lc-rs as the crypto backend for all TLS operations across the entire dependency tree. It relies only on documented Windows APIs (
BCryptGenRandom). Theringcrate, which calls the undocumentedSystemFunction036(RtlGenRandom) and can trigger false positives in behaviour-based AV scanners, is not present in the binary.
# With cross (requires Docker)
cargo install cross
cross build --release --target x86_64-pc-windows-gnu
# With MinGW (no Docker)
sudo apt install gcc-mingw-w64-x86-64
rustup target add x86_64-pc-windows-gnu
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc \
cargo build --release --target x86_64-pc-windows-gnuntfy-rs can be used as a library crate embedded in other Rust applications. The server runs as a background thread with graceful shutdown support.
use ntfy_rs::{start, ServerHandle};
use ntfy_rs::config::{Config, ServeArgs};
// Build config
let args = ServeArgs {
listen_http: Some(":8090".to_string()),
base_url: Some("http://192.168.0.82:8090".to_string()),
upstream_base_url: Some("https://ntfy.sh".to_string()),
..Default::default()
};
let config = Config::resolve(args)?;
// Start server on a background thread
let handle: ServerHandle = start(config)?;
// Publish a notification
handle.publish("mytopic", "Title", "Hello from Rust", "high")?;
// Graceful shutdown
handle.shutdown();| Method | Description |
|---|---|
publish(topic, title, message, priority) |
Send a notification via HTTP POST to the local server |
shutdown() |
Signal the server to stop and wait for it to finish |
The CLI binary (ntfy-rs serve ...) is a thin wrapper around this same library API.
ntfy-rs uses Cargo feature flags to allow consumers to disable unused functionality and reduce binary size. All features are enabled by default; disable them with default-features = false and enable only what you need.
| Feature | Default | Description | Key dependencies removed when disabled |
|---|---|---|---|
email |
yes | Outbound SMTP email notifications | lettre (+16 transitive deps) |
metrics |
yes | Prometheus metrics exposition (/metrics) |
metrics, metrics-exporter-prometheus |
tls |
yes | HTTPS listener (rustls + aws-lc-rs) | axum-server, rustls |
webpush |
yes | Web Push notifications (VAPID/ECE) | p256, aes-gcm, hkdf |
auth |
yes | Authentication: Basic, Bearer, ACL | bcrypt |
unix-socket |
yes | Unix domain socket listener | hyper, hyper-util |
config-file |
yes | TOML config file + CLI arg parsing | clap, config |
Note: SQLite (rusqlite, r2d2, r2d2_sqlite) is always required — it is deeply embedded in the server's publish/subscribe/manager pipeline.
For embedded use cases that only need core publish/subscribe (no email, no metrics, no TLS, no web push, no auth, no Unix socket, no config file):
[dependencies]
ntfy-rs = { git = "...", default-features = false }This removes ~10–15 MB of compiled dependencies from the final binary.
# In-memory cache, no auth, port 2586 (see "Default port" below)
ntfy-rs serve
# Persistent cache
ntfy-rs serve --cache-file /var/lib/ntfy-rs/cache.db
# With config file
ntfy-rs serve --config /etc/ntfy-rs/server.tomlOn Windows, the server requires inbound TCP access on its port. On first launch, Windows Defender Firewall will show a prompt:
"Windows Defender Firewall has blocked some features of this app"
- Check "Private networks" (your home/work LAN)
- Uncheck "Public networks" (coffee shops, hotels — no need to expose ntfy there)
- Click Allow
If you accidentally deny or miss the prompt, run these commands in elevated PowerShell:
# Ensure your network is classified as Private
Set-NetConnectionProfile -Name "<NetworkName>" -NetworkCategory Private
# Remove any existing ntfy firewall rules (optional cleanup)
Get-NetFirewallRule | Where-Object { $_.DisplayName -like "*ntfy*" } | Remove-NetFirewallRule
# Add firewall rule
New-NetFirewallRule -DisplayName "ntfy-rs Server" -Direction Inbound -Protocol TCP -LocalPort 2586 -Action Allow -Profile PrivateIf you change the port, update the firewall rule to match.
All settings can be provided via config file (TOML), CLI flag, or NTFY_* environment variable. CLI flags override the config file.
# /etc/ntfy-rs/server.toml
listen-http = ":2586"
listen-https = ":443" # optional; requires cert-file + key-file
listen-unix = "/run/ntfy-rs/ntfy-rs.sock" # optional
# The public URL clients use to reach this server.
# MUST include the port number — the iOS app includes the port when
# registering with ntfy.sh for APNs, and the upstream poll-forward
# hashes the full URL. A mismatch means lock screen notifications
# will not arrive on iOS. (Android is unaffected — it uses a
# persistent WebSocket, not APNs.)
base-url = "https://ntfy.example.com:443"
cache-file = "/var/lib/ntfy-rs/cache.db"
attachment-cache-dir = "/var/lib/ntfy-rs/attachments"
# How long messages are retained (seconds). Default: 43200 (12 hours)
cache-duration = 43200
# Maximum message body size (bytes). Default: 4096
message-size-limit = 4096
# TLS
cert-file = "/etc/letsencrypt/live/ntfy.example.com/fullchain.pem"
key-file = "/etc/letsencrypt/live/ntfy.example.com/privkey.pem"
# Auth (optional). When set, auth is enabled.
auth-file = "/var/lib/ntfy-rs/auth.db"
default-access = "read-write" # read-write | read-only | deny-all
# Rate limiting
request-limit-burst = 60 # requests allowed in a burst
request-limit-replenish = 5 # seconds to replenish one token
# Subscription limit per IP
subscription-limit = 30
# Keepalive interval for SSE/WS connections (seconds). Default: 45
keepalive-interval = 45
# Background manager interval (seconds). Default: 180
# Controls delayed message delivery granularity.
manager-interval = 180
# Delayed message maximum (seconds). Default: 259200 (3 days)
max-delay-secs = 259200
# iOS upstream poll-forward — required for iOS lock screen notifications.
# Without this, iOS clients only receive notifications while the app is open.
upstream-base-url = "https://ntfy.sh"
upstream-access-token = "" # optional Bearer token for upstream# server.toml — LAN-only setup, no TLS
listen-http = ":2586"
# base-url must include the port for iOS lock screen notifications to work.
base-url = "http://192.168.0.82:2586"
upstream-base-url = "https://ntfy.sh"
cache-file = "cache.db"
attachment-cache-dir = "attachments"See the TLS → With a self-signed certificate section for the full step-by-step guide.
# server.toml — LAN with self-signed HTTPS
listen-http = ":2586"
listen-https = ":443"
base-url = "https://192.168.0.82:443" # must include port
upstream-base-url = "https://ntfy.sh"
cache-file = "cache.db"
attachment-cache-dir = "attachments"
cert-file = "server-fullchain.crt" # server cert + CA cert
key-file = "server.key"Every config key maps to NTFY_<KEY> in uppercase with underscores:
| Variable | Example |
|---|---|
NTFY_LISTEN_HTTP |
:2586 |
NTFY_LISTEN_HTTPS |
:443 |
NTFY_LISTEN_UNIX |
/run/ntfy-rs/ntfy.sock |
NTFY_BASE_URL |
https://ntfy.example.com |
NTFY_CACHE_FILE |
/var/lib/ntfy-rs/cache.db |
NTFY_AUTH_FILE |
/var/lib/ntfy-rs/auth.db |
NTFY_LOG_LEVEL |
info |
RUST_LOG |
ntfy_rs=debug |
# Minimal
curl -d "Hello" ntfy.example.com/mytopic
# With headers
curl -H "Title: Deployment done" \
-H "Priority: high" \
-H "Tags: white_check_mark" \
-d "Server restarted" \
ntfy.example.com/mytopic
# Delayed delivery (30 minutes from now)
curl -H "Delay: 30m" -d "Reminder" ntfy.example.com/mytopic
# Delay formats: 30s, 5m, 2h, 1d, Unix timestamp, RFC 3339
# Upload a file attachment
curl -H "Title: Photo" -H "X-Filename: photo.jpg" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg \
ntfy.example.com/mytopic
# External attachment URL (no upload)
curl -H "Title: Image test" -H "Attach: https://ntfy.sh/static/img/ntfy.png" \
-d "Check this out" ntfy.example.com/mytopic
# Action buttons
curl -H "Title: Server down" \
-H "Actions: view, Open dashboard, https://grafana.example.com; http, Restart, https://api.example.com/restart, method=POST, clear=true" \
-d "CPU at 100%" ntfy.example.com/mytopic| Header | Aliases | Description |
|---|---|---|
X-Title |
Title, t |
Message title |
X-Priority |
Priority, prio, p |
1/min · 2/low · 3/default · 4/high · 5/urgent |
X-Tags |
Tags, tag, ta |
Comma-separated emoji shortcodes (auto-resolved to unicode, e.g. white_check_mark → ✅). Raw emoji characters are not recognized — use shortcodes (e.g. tada, not 🎉). |
X-Click |
Click |
URL to open on click |
X-Icon |
Icon |
Icon URL |
X-Markdown |
Markdown, md |
1 to render body as Markdown |
X-Actions |
Actions, action |
Action buttons (see SPEC.md for format) |
X-Encoding |
Encoding, enc, e |
base64 to send a binary body |
X-Attach |
Attach |
External attachment URL (no file upload) |
X-Filename |
Filename |
Filename for file attachment upload |
X-Delay |
Delay, X-At, At, X-In, In |
Scheduled delivery time |
Content-Type |
text/markdown sets Markdown rendering; non-text type triggers attachment upload |
iOS note: The ntfy iOS app (as of v1.6) does not render attachment image previews in notifications or in-app. The Android app shows a download link but not an inline preview. Attachments are served correctly by ntfy-rs; image display is an app-side feature that has not shipped yet. Action buttons (
X-Actions) render in-app on both platforms but not on the iOS lock screen or Notification Center — only Android shows them in notifications.
# NDJSON stream (primary — used by ntfy clients)
curl -s ntfy.example.com/mytopic/json
# Poll (return cached messages and exit)
curl -s "ntfy.example.com/mytopic/json?poll=1"
# Since a specific time
curl -s "ntfy.example.com/mytopic/json?since=1712345678"
# All cached messages
curl -s "ntfy.example.com/mytopic/json?since=all"
# SSE (browser EventSource)
curl -s ntfy.example.com/mytopic/sse
# Multiple topics
curl -s ntfy.example.com/topic1,topic2/json
# WebSocket (used by ntfy Android app)
# ws://ntfy.example.com/mytopic/wsThe subscription URL in the ntfy app must match base-url (including the port) for iOS lock screen notifications to work. The APNs hash is derived from the full URL, so a mismatch means the app won't be woken by push notifications.
| Scenario | Server base-url |
App subscription URL | Extra steps |
|---|---|---|---|
| Public server, real domain | https://ntfy.example.com:443 |
https://ntfy.example.com:443 |
None — real cert is trusted automatically |
| LAN, HTTP only | http://192.168.0.82:2586 |
http://192.168.0.82:2586 |
None — simplest setup |
| LAN, HTTPS with self-signed cert | https://192.168.0.82:443 |
https://192.168.0.82:443 |
Install CA cert on every device (see TLS section) |
Tip: For LAN-only setups, HTTP is the simplest option — no certificates to manage, no CA to install on devices. Everything works including iOS lock screen notifications.
Auth is disabled by default. Set auth-file to enable it.
auth-file = "/var/lib/ntfy-rs/auth.db"
default-access = "deny-all"# Register via API
curl -X POST ntfy.example.com/v1/account \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}'
# Promote to admin directly in the DB
sqlite3 /var/lib/ntfy-rs/auth.db \
"UPDATE users SET role='admin' WHERE username='admin';"# Register
curl -X POST ntfy.example.com/v1/account \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"pass"}'
# Get own account info
curl -u alice:pass ntfy.example.com/v1/account
# Change password
curl -u alice:pass -X PUT ntfy.example.com/v1/account/password \
-H "Content-Type: application/json" \
-d '{"password":"newpass"}'
# Create Bearer token
curl -u alice:pass -X POST ntfy.example.com/v1/account/token \
-H "Content-Type: application/json" \
-d '{"label":"my-app","expires":1800000000}'
# Revoke token
curl -u alice:pass -X DELETE ntfy.example.com/v1/account/token/tk_...
# Grant topic access
curl -u alice:pass -X POST ntfy.example.com/v1/account/access \
-H "Content-Type: application/json" \
-d '{"topic":"mytopic","read":true,"write":true}'
# Delete own account
curl -u alice:pass -X DELETE ntfy.example.com/v1/account# List all users
curl -u admin:secret ntfy.example.com/v1/admin/users
# Create user
curl -u admin:secret -X POST ntfy.example.com/v1/admin/users \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"pass","role":"user"}'
# Change role
curl -u admin:secret -X PUT ntfy.example.com/v1/admin/users/bob/role \
-H "Content-Type: application/json" \
-d '{"role":"admin"}'
# Set ACL for user
curl -u admin:secret -X POST ntfy.example.com/v1/admin/users/bob/access \
-H "Content-Type: application/json" \
-d '{"topic":"alerts","read":true,"write":false}'
# Delete user
curl -u admin:secret -X DELETE ntfy.example.com/v1/admin/users/bobUse a CA-signed certificate (e.g., Let's Encrypt). No special client configuration needed — iOS trusts it automatically.
listen-https = ":443"
cert-file = "/etc/letsencrypt/live/ntfy.example.com/fullchain.pem"
key-file = "/etc/letsencrypt/live/ntfy.example.com/privkey.pem"For servers on a local network without a public domain, you can use a self-signed certificate. This requires installing a custom CA on every iOS device that will connect — it is not practical for large deployments.
Tip: If you don't need encryption on your local network, HTTP works fine for both in-app and lock screen notifications. Self-signed HTTPS is only needed if you want TLS on a LAN without a public domain.
Run these commands in a terminal (Git Bash on Windows, or any shell on Linux/macOS):
# --- Create the local CA ---
openssl req -x509 -new -nodes -newkey rsa:2048 \
-keyout localCA.key -out localCA.crt \
-days 3650 \
-subj "/C=US/ST=State/L=Local/O=MyPrivateCA/CN=My Local Root CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash"
# --- Create the server certificate signing request ---
# Replace 192.168.0.82 with your server's IP address.
# You can also add a DNS name (e.g., DNS:myserver.local) to the SAN below.
openssl req -new -nodes -newkey rsa:2048 \
-keyout server.key -out server.csr \
-subj "/CN=192.168.0.82"
# --- Create the extensions config ---
cat > san.ext << 'EOF'
subjectAltName = IP:192.168.0.82
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
basicConstraints = CA:FALSE
EOF
# --- Sign the server certificate with the CA ---
openssl x509 -req -in server.csr \
-CA localCA.crt -CAkey localCA.key -CAcreateserial \
-out server.crt -days 825 -extfile san.ext
# --- Build the fullchain certificate (server cert + CA) ---
cat server.crt localCA.crt > server-fullchain.crtWindows (Git Bash): Prefix the
openssl reqcommands withMSYS_NO_PATHCONV=1to prevent Git Bash from mangling the-subjpath argument. For example:MSYS_NO_PATHCONV=1 openssl req -x509 -new -nodes -newkey rsa:2048 \ -keyout localCA.key -out localCA.crt \ -days 3650 \ -subj "/C=US/ST=State/L=Local/O=MyPrivateCA/CN=My Local Root CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" MSYS_NO_PATHCONV=1 openssl req -new -nodes -newkey rsa:2048 \ -keyout server.key -out server.csr \ -subj "/CN=192.168.0.82"
listen-https = ":443"
cert-file = "server-fullchain.crt" # fullchain, not just server.crt
key-file = "server.key"
base-url = "https://192.168.0.82:443" # must include portiOS:
- Transfer
localCA.crtto the iPhone (AirDrop, email, or host it on the server). - Open the file — iOS will prompt to install a configuration profile. Tap Install → Install.
- Go to Settings → General → About → Certificate Trust Settings. Toggle full trust ON for "My Local Root CA".
- Verify: open
https://192.168.0.82/v1/configin Safari. It should load without any warning.
Android:
- Transfer
localCA.crtto the Android device. - Go to Settings → Security → Install from storage (path varies by device; may be under Settings → Security & privacy → Encryption & credentials → Install a certificate → CA certificate).
- Select the
.crtfile and confirm. - Verify: open
https://192.168.0.82/v1/configin Chrome. It should load without any warning.
Note: On Android 7+, apps that don't explicitly opt in to trusting user-installed CAs will still reject self-signed certificates. The ntfy Android app does trust user CAs, so attachments and subscriptions work after installing the CA.
Use the full URL including port: https://192.168.0.82:443. The app must connect with the same URL that base-url is set to, or lock screen notifications will not work.
If listen-https is set but cert-file/key-file are missing, a warning is logged and the server continues on HTTP only. Certificate hot-reload is not supported; restart the server to pick up a new cert.
ntfy-rs acts as a Matrix Push Gateway for UnifiedPush. Set base-url and point your Matrix homeserver's pusher URL to /_matrix/push/v1/notify.
# Discovery
curl ntfy.example.com/_matrix/push/v1/notify
# → {"unifiedpush":{"gateway":"matrix"}}The pushkey must be a full ntfy topic URL on this server, e.g. https://ntfy.example.com/upXXXXXXXX?up=1. Pushkeys from other servers are returned as rejected per the Matrix spec.
Apple's Apple Push Notification service (APNs) is the only way to deliver push notifications to iOS devices when an app is in the background. Unlike Android, which can maintain a persistent connection to any server, iOS apps can only be woken in the background by APNs — and only apps distributed through the App Store can use APNs with their own certificate.
This means a self-hosted ntfy-rs server cannot wake the iOS ntfy app directly. The ntfy iOS app is signed by the ntfy.sh developer and registered with APNs under the ntfy.sh certificate. Only ntfy.sh can trigger a wake-up.
ntfy-rs works around this with upstream poll-forward:
- A message is published to your ntfy-rs server.
- ntfy-rs sends a lightweight wake signal to
upstream-base-url(ntfy.sh). The topic is hashed (sha256(base-url + "/" + topic)) so ntfy.sh never learns the actual topic name or message content. - ntfy.sh triggers APNs, which wakes the iOS app.
- The app polls your ntfy-rs server for the actual message.
Message content never passes through ntfy.sh — it only carries the opaque hash and a message ID. This is why upstream-base-url and base-url must be configured correctly:
base-urlis required — the topic hash is derived from the full topic URL (base-url/topic). Without it the wrong hash is sent and iOS notifications will not arrive.base-urlmust exactly match the URL the iOS app uses to connect, including the port number — even for default ports like 443 or 80 (e.g.https://192.168.0.82:443, nothttps://192.168.0.82). The iOS app includes the port in its registration URL, so a mismatch causes the hash to differ and APNs wake-ups will not reach the device.- Android is unaffected — Android clients maintain a persistent WebSocket to your server and don't use APNs.
Lock screen appearance: Because the poll request only carries a topic hash and a generic "New message" fallback (not the actual message content), iOS lock screen notifications show a generic alert. The full title, message, and tags are visible when you open the app. This is a limitation of the poll-forward architecture, not a bug — the Go ntfy server has the same behavior when self-hosted.
base-url = "http://192.168.0.82:2586"
upstream-base-url = "https://ntfy.sh"
upstream-access-token = "" # optional; set if you have a ntfy.sh account with higher rate limitsOr via CLI flags:
.\ntfy-rs.exe serve --listen-http :2586 --base-url http://192.168.0.82:2586 --upstream-base-url https://ntfy.shntfy-rs can email you (or any address) whenever a message is published. Useful for alerting, SMS gateways, or as a fallback channel.
smtp-host = "smtp.gmail.com"
smtp-port = 587 # optional, default 587 (STARTTLS)
smtp-starttls = true # optional, default true. Set false for local testing (e.g. Mailpit)
smtp-username = "you@gmail.com"
smtp-from = "ntfy-rs <you@gmail.com>"
smtp-to = ["you@gmail.com", "5551234567@txt.carrier.com"]
smtp-min-priority = 3 # optional: only email priority >= 3 (default 0 = all)
# Password — choose the most secure option available:
smtp-password = "app-password" # least preferred (plaintext in config)
smtp-password-file = "/run/secrets/smtp_pw" # preferred for Docker / systemd
# NTFY_SMTP_PASSWORD env var # most preferredEmail delivery is fire-and-forget — failures are logged but never cause a publish to fail. Email for delayed messages is sent at delivery time, not at scheduling time.
# Log level via flag
ntfy-rs serve --log-level debug
# Per-module filtering via RUST_LOG
RUST_LOG=ntfy_rs=debug,tower_http=warn ntfy-rs serventfy-rs defaults to :2586 rather than the Go ntfy default of :80. The reasons:
- Privilege-free by default. On Linux and macOS, binding ports below 1024 requires root or a specific capability (
CAP_NET_BIND_SERVICE). Using:80as the default would cause an immediatepermission deniederror for any user running ntfy-rs as a normal process — the dominant use case for a single-binary server. - Windows parity. On Windows, port 80 is frequently occupied by IIS, HTTP.sys, or other services.
:2586works out-of-the-box without conflicts. - Protocol, not operational, compatibility. ntfy-rs is wire-compatible with ntfy clients at the API level (message format, headers, routes). It does not aim to match the Go server's deployment assumptions, which are built around managed system packages, systemd units, and elevated privileges.
If you need to serve on port 80 or 443 without TLS termination by a reverse proxy, either run the binary with the required privilege or set the address explicitly:
# server.toml — match Go ntfy defaults
listen-http = ":80"
listen-https = ":443"# Linux: grant binding capability without running as root
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/ntfy-rsFor production, the recommended approach on all platforms is to run ntfy-rs on its default port behind a reverse proxy (nginx, Caddy, Traefik) that handles TLS and listens on 80/443.
- CORS: ntfy-rs uses permissive CORS by default (all origins allowed). In production, configure your reverse proxy to override CORS headers and restrict allowed origins.
- Metrics endpoint: The
/metricsendpoint is unauthenticated and exposes message counts and topic activity. Restrict access via your reverse proxy or firewall rules. - Auth file: When
auth-fileis set, all publish/subscribe and account endpoints require authentication. Admin endpoints additionally require an admin-role user.
ntfy-rs is a ground-up Rust reimplementation targeting a smaller binary and zero system dependencies, while maintaining full wire compatibility with ntfy clients. It is not a port of the Go codebase.
| Feature | ntfy (Go) | ntfy-rs (Rust) |
|---|---|---|
HTTP publish (PUT/POST /{topic}) |
✅ | ✅ |
NDJSON stream (/{topic}/json) |
✅ | ✅ |
SSE stream (/{topic}/sse) |
✅ | ✅ |
WebSocket (/{topic}/ws) |
✅ | ✅ |
| Multi-topic subscriptions | ✅ | ✅ |
Poll mode (?poll=1, ?since=) |
✅ | ✅ |
Scheduled / delayed delivery (X-Delay) |
✅ | ✅ |
| Title, priority, tags (emoji shortcode resolution), click URL, icon | ✅ | ✅ |
Markdown rendering (X-Markdown) |
✅ | ✅ |
Action buttons (X-Actions) |
✅ | ✅ |
| File attachments (local disk storage) | ✅ | ✅ |
File attachments (external URL, X-Attach) |
✅ | ✅ |
| File attachments (S3 / remote storage) | ✅ | ❌ |
| Attachment image preview in iOS app | ❌ | ❌ |
| Base64-encoded binary body | ✅ | ✅ |
| Feature | ntfy (Go) | ntfy-rs (Rust) |
|---|---|---|
| Basic auth & Bearer token | ✅ | ✅ |
| Per-topic ACL | ✅ | ✅ |
| Anonymous access tiers (read-write / read-only / deny-all) | ✅ | ✅ |
In-memory ACL cache (auth-access-cache) |
✅ | ❌ |
User self-service API (/v1/account) |
✅ | ✅ |
Admin user management API (/v1/admin) |
✅ | ✅ |
| Bearer token creation & revocation | ✅ | ✅ |
| Feature | ntfy (Go) | ntfy-rs (Rust) |
|---|---|---|
| HTTP listener | ✅ | ✅ |
| HTTPS / TLS (built-in) | ✅ (Go stdlib) | ✅ (rustls + aws-lc-rs) |
| Unix domain socket | ✅ | ✅ (Linux/macOS) |
| Per-IP rate limiting | ✅ | ✅ |
| Per-IP subscription limits | ✅ | ✅ |
| SQLite message cache | ✅ (cgo + system lib) | ✅ (bundled, no cgo) |
| Config file | ✅ (YAML) | ✅ (TOML) |
| Environment variables | ✅ | ✅ |
| CLI flags | ✅ | ✅ |
--version flag |
✅ | ✅ |
| Prometheus metrics endpoint | ✅ | ✅ |
| Web app (React SPA) | ✅ | ❌ |
| Embeddable library | ❌ | ✅ |
| Native Windows binary (no cgo) | ❌ | ✅ |
| Default port | :80 (requires root/cap) |
:2586 (unprivileged) |
| Binary size (release, uncompressed) | ~21 MB | ~5–8 MB |
| Feature | ntfy (Go) | ntfy-rs (Rust) |
|---|---|---|
| iOS upstream poll-forward (APNs via ntfy.sh) | ✅ | ✅ |
| UnifiedPush / Matrix Push Gateway | ✅ | ✅ |
| Firebase Cloud Messaging (FCM) | ✅ (optional) | ❌ |
| Web Push / VAPID | ✅ | ✅ |
| SMTP ingress (publish via email) | ✅ | ❌ |
| Email notifications (outbound) | ✅ | ✅ |
| Phone call notifications (Twilio) | ✅ | ❌ |
| Stripe billing / usage tiers | ✅ (ntfy.sh only) | ❌ |
| PostgreSQL | supported | not yet |
Some features present in ntfy (Go) are intentionally absent and are unlikely to be added:
Firebase Cloud Messaging (FCM) FCM requires a Google account, a project API key, and sending notifications through Google's servers. ntfy-rs is designed for self-hosted deployments where avoiding proprietary third-party infrastructure is the point. Android clients using UnifiedPush work without FCM.
React web app The upstream ntfy.sh web app is a separate React SPA that works against any wire-compatible server, including ntfy-rs. Bundling it into the server binary would add significant build complexity for something that is already available and maintained upstream.
Twilio voice calls and Stripe billing These are third-party paid services integrated into the ntfy.sh hosted offering. They have no role in a self-hosted server binary.
Features listed as "not yet" in the table above (PostgreSQL, SMTP ingress) are planned but not yet implemented.
Dual-licensed under Apache-2.0 and GPL-2.0. You may use this software under the terms of either license, at your option.
This project is not affiliated with, endorsed by, or connected to the Rust Project or the Rust Foundation.
ntfy-rs is an independent reimplementation and is not affiliated with or endorsed by the original ntfy project (binwiederhier/ntfy).
