Back to Blog
Galen Guan

ntfy vs Gotify vs Nostr: A Deep Comparison of Self-Hosted Push — How to Get Your AI Agent's Cron Results Onto Your Phone

Everyone who self-hosts an AI agent hits the same wall around day three: the 3 AM cron run finishes, produces twenty competitor updates and one failed dependency upgrade, and you're asleep with a phone as silent as a brick. You open the terminal the next morning and discover a job died overnight, and you've already missed the window.

The problem isn't where the results are stored — they're sitting in the database. The problem is how the results find you. Email is slow and buried. WeChat bots mean wrangling WeCom/Feishu and fighting risk-control systems. SMS costs money. The answer that actually works is a self-hosted push channel: let your agent shove a message onto your lock screen with a single HTTP request.

This space has three main contenders: ntfy, Gotify, and Nostr Relay. All three are self-hostable, open source, and can send a message from a one-line curl. But their architectural assumptions are completely different — pick the wrong one and your iPhone gets nothing, or your messages wait forever for an app to be woken up. As of August 2026, I've walked through the source and the deployment flow of each. Here's where I landed.

The Core Difference: Architecture First

Before diving into details, here's the architectural overview — all three call themselves "push services," but their data flows and dependencies are three entirely different beasts:

Architecture comparison of ntfy, Gotify, and Nostr Relay self-hosted push solutions

One sentence to frame it: ntfy is "minimal HTTP pub/sub," Gotify is "a WebSocket server with a user system," and Nostr is "a decentralized event broadcast protocol." That difference drives every trade-off that follows.

ntfy.sh: Boiling "Push" Down to a Single curl

ntfy (binwiederhier/ntfy) is the most restrained project I've seen in this space. Its entire core design is one sentence: HTTP POST to a topic, and every subscriber to that topic receives the message.

curl -d "build failed" ntfy.sh/mytopic

No SDK, no registration, no handshake. The only agreement between publisher and subscriber is that topic string — it's both the address and the credential. That produces two direct advantages:

First, zero-friction publishing. Anything that can make an HTTP request — shell scripts, Python, Go, CI systems, even watch curl — can be a publisher. For an AI agent, that means you don't need to bolt an ntfy-specific SDK into its toolchain; one line of curl or one httpx.post() does it. This is the fundamental gap versus Gotify, which requires you to create an app and fetch a token first.

Second, full-platform subscriber coverage. ntfy ships official iOS, Android, and Web clients (all actively maintained as of August 2026), and this is the key point where it beats Gotify. The mechanism underneath matters more: the official Android app supports FCM (Firebase Cloud Messaging), and the iOS app supports APNs (Apple Push Notification). That means even when the app is killed or frozen by the OS, messages still reach the lock screen through the system-level push channel — the single hardest thing to get right in self-hosted push, and the one most easily overlooked.

This design decision deserves to be spelled out: many self-hosted push setups (Gotify included, and any pure-WebSocket approach) rely on keeping a long-lived connection alive to receive messages. But iOS's background policy will strangle that connection, causing delayed or lost messages. ntfy's approach is to run FCM/APNs forwarding on its public instance, with your self-hosted instance as the upstream — when you docker run it, you configure an upstream public server, so messages hit your own server first, then borrow the public instance's FCM/APNs channel for system-level delivery. That hybrid architecture is the root cause of its instant delivery.

The rest fits on one page: SQLite/PostgreSQL dual backends, attachment support (local filesystem or S3, 3-hour default expiry), message caching (cache-duration configurable, 12-hour default), priorities (1–5), Markdown rendering, click actions, email forwarding. Access control supports auth-file (username/password) plus a deny-all baseline — registration and subscription are denied by default, and you have to explicitly allow them in auth.yml. Its security defaults are the most conservative of the three.

The limits are also explicit: the free public instance ntfy.sh caps each topic at 512 messages per day, and your messages are visible to anyone who can guess the topic name (topic-as-password; a long random string is effectively a password). For privacy, self-host — or self-host and add an FCM/APNs upstream.

Gotify: A Beautiful, Android-Only Push Server

Gotify (gotify/server) is the one that feels most like a "complete product." It's written in Go, MIT-licensed, starts with a single Docker command, and ships a React-based Web UI for managing users, creating apps, issuing app tokens, and browsing message history.

Architecturally it diverges from ntfy on a fundamental point: Gotify delivers via a persistent WebSocket connection, with no reliance on FCM/APNs-style system channels. That yields one genuinely nice property — once self-hosted, it has zero external dependencies, messages travel entirely through your own server, latency is extremely low (<50ms measured on a LAN), and nothing ever touches Google's or Apple's push servers. Messages are persisted to SQLite, history is searchable, and image attachments go straight through the extras field.

But its fatal flaw is written on its face: no iOS client. As of August 2026, Gotify officially provides only an Android app and a web interface. iOS users are stuck with a web app or third-party workarounds — which, under iOS's background restrictions, effectively means "no real-time notifications." On Android it's actually solid — the app receives messages in real time over the WebSocket connection, with FCM as a fallback — but a push solution that drops the entire iOS user base is a one-strike disqualifier for most people.

The user system is a double-edged sword: the user → app → token three-tier model is more "formal" than ntfy's topics and suits multi-user, multi-app scenarios with an admin UI. But for a lightweight "agent sends one message" use case, it's added ceremony — you have to create an app and generate a token instead of just typing a topic name.

Nostr Relay: Decentralization Done Right, With a Counterintuitive Push Weakness

Nostr wasn't designed for push. It's a decentralized social protocol (NIP-01 defines Events with Schnorr signatures; relays store and forward them). But precisely because it natively supports "one pubkey broadcasts to any relay, any client subscribes," plenty of people repurpose it as a self-hosted push channel.

Sending a message means constructing a signed Event with pynostr (Python) or a JS SDK and stuffing it into your relay:

from pynostr.key import PrivateKey
from pynostr.relay_manager import RelayManager
from pynostr.event import Event

key = PrivateKey()
relay = RelayManager()
relay.add_relay("wss://relay.example.com")
event = Event("build failed", kind=1, pubkey=key.public_key.hex())
key.sign_event(event)
relay.publish_event(event)

Its strengths are the real deal: decentralization (relays can be redundantly deployed; if one goes down the message lives on another), censorship resistance, NIP-04/NIP-44 encrypted DMs, permanent relay storage under policy, and an open client ecosystem (Damus/iOS, Amethyst/Android, Iris/Web are all ready to use).

But "push" is precisely its weakest link. This is the point I most want to flag: Nostr clients receive messages by keeping the app in the foreground or at least maintaining a background connection, and iOS's restrictions on background WebSockets mean messages simply won't reach the lock screen once the app is frozen by the OS. In other words, using it as "a social feed you actively refresh" works perfectly — but using it as "the channel that notifies you when your agent finishes a 3 AM job" means the message will likely only arrive when you open the app the next day. This flaw isn't fixable by configuration; it's baked into the protocol's positioning. Nostr has never promised system-level push.

Seven-Dimension Comparison

Put the three into one table and the choice becomes self-evident:

Dimension ntfy Gotify Nostr Relay
Protocol HTTP POST/PUB + WebSocket/SSE subscribe, topic-as-address REST + persistent WebSocket, registration required NIP-01 Event + Schnorr signature, open protocol
Clients ✅ iOS + Android + Web, official everywhere ❌ Android + Web only, no iOS ✅ iOS + Android + Web, but no native push
Security ACL + Token + IP rate limit, deny-all baseline User + App Token + TLS Pubkey signature + encrypted DM, no server trust
Self-host effort Docker one-liner, minimal Docker one-liner, minimal Relay + DB + reverse proxy, medium
Latency ~1.1s (public) / <100ms (self-hosted), instant via FCM/APNs <50ms (self-hosted WebSocket) ~50-200ms, but requires foreground app
Attachments ✅ S3/local, 3h default expiry ✅ images/files (extras field) ⚠️ URL references only (NIP-94/92)
Message history ✅ configurable cache (12h default) ✅ permanent SQLite persistence ✅ relay storage, multi-relay redundancy

Seven-dimension feature matrix comparing ntfy, Gotify, and Nostr

Scoring

Dimension ntfy Gotify Nostr
Protocol simplicity 9/10 7/10 6/10
Client coverage 9/10 4/10 8/10
Security model 8/10 8/10 9/10
Self-host effort 9/10 9/10 6/10
Push latency 8/10 9/10 4/10
Attachment support 8/10 7/10 5/10
Message history 7/10 9/10 9/10
Overall 8.3 7.6 6.7

The rationale in one line: ntfy's protocol simplicity and full-platform coverage are decisive (for the push use case, those two dimensions carry the most weight); Gotify dies on client coverage, but its latency and history are the strongest of the three; Nostr's security and history are excellent, but a 4/10 on push latency drags the total down — a push solution with bad latency loses no matter how strong its other dimensions are.

Conclusion: Pick by Scenario, Not by Faith

My recommendation is direct, no hedging:

  • For AI agent push, choose ntfy. No contest. Your agent sends a message with a single curl or httpx.post() — no SDK, no app creation, no token ceremony. iOS/Android/Web all covered, FCM/APNs guarantee instant lock-screen delivery, Docker one-liner to self-host, and deny-all plus a long random topic gives you a private channel that's good enough. Of the three, it's the only one that was built for push. If you're assembling your own agent infrastructure, this topic pairs naturally with my earlier breakdown of the Bitchat Mesh protocol (node-to-node communication) and Iroh 1.0's dial-keys-not-ips (peer addressing) — together they form three complementary sides of "agent communication": how nodes connect, how identities address each other, and how results get pushed to you.

  • For a pure-Android environment, choose Gotify. If you and your users are all on Android, and you care about "messages never touch Google/Apple servers," Gotify's persistent WebSocket + low latency + permanent history + Web UI is the most comfortable. But if there's even a 1% chance you'll use iOS someday, don't touch it.

  • For the decentralization faithful, choose Nostr. If what you want is censorship resistance, an open protocol, permanent message storage, and integration with an existing Nostr identity, it's the only option. But cross "push" off your list of expectations — it's a feed, not a notification. If you insist on using it, treat it as the archive layer for agent messages (relays store history permanently), and hook a separate ntfy instance onto the notification leg. The two don't conflict.

The honest closing note: none of these three is a silver bullet, but ntfy is the only one that gets every link in the "cron to lock screen" chain right without forcing you to compromise. On the pain-point checklist for self-hosted AI agents, push is one item ntfy has already crossed off.

References

  1. ntfy — ntfy.sh official docs (publish/subscribe/config/publish endpoints) (continuously updated)
  2. ntfy — GitHub repo binwiederhier/ntfy (Go, dual Apache-2.0/GPL-2.0)
  3. ntfy — iOS app (App Store)
  4. ntfy — Android app (Google Play)
  5. Gotify — GitHub repo gotify/server (Go, MIT)
  6. Gotify — official docs (REST API / WebSocket / deployment)
  7. Gotify — Android app
  8. Nostr — NIP-01 protocol spec (Event structure and relay behavior)
  9. Nostr — NIP-04 encrypted DM / NIP-44 versioned encryption
  10. pynostr — Python Nostr library (for sending messages)
  11. Nostr relay — nostream (TypeScript relay implementation)
  12. Nostr clients — Damus (iOS) / Amethyst (Android)