The Problem

Developers need a lightweight, self‑hosted push‑notification service that can be triggered from scripts or CI pipelines without managing a full‑blown messaging platform. Existing solutions often require external cloud accounts, proprietary SDKs, or complex deployment steps, creating friction for on‑premise or air‑gapped environments.

What This Does

ntfy implements an HTTP‑based pub/sub broker that accepts PUT/POST requests and forwards messages to registered subscribers (mobile app, desktop client, or web UI). The core server lives in server/ (server.go, server_test.go) and the web UI in web/src/. The command‑line client (client/client.go) and helper binaries under cmd/ (publish.go, access.go) provide a simple API for producers. Configuration files such as client/client.yml and the Docker compose file (docker-compose.yml) let you run a complete stack with a single docker compose up.

How It Is Wired

  1. Entry point – The HTTP server starts in server/server.go via func handle (line 506). This handler is registered with the Go HTTP mux and is the only place that reaches 266 internal functions.
  2. Authentication flowhandle calls maybeAuthenticate, which invokes AuthAllowedTokens. Tokens performs a database query (db.Query) defined in db/db.go. This is the primary outbound DB interaction (1 of 91 DB‑touching functions).
  3. Message routing – After auth, the request is parsed and passed to execServe (in server/server.go). execServe creates a Message object and calls New (41 calls) and ParseDuration (12 calls) before pushing it into the in‑memory cache (createMessageCache).
  4. Outbound effects – The only network calls are the HTTP responses to subscribers; the server itself does not initiate external HTTP requests. Filesystem writes occur in attachment/store.go (store attachments) and client/client.go (client config).
  5. High‑impact hubsweb/src/app/utils.js and web/src/components/routes.js are imported by 23 modules each; changes here have a large blast radius. In the Go code, forEachBackend is called from 400 places, making it a critical change point.
  6. Hot spots – Deep nesting (max depth 9) appears in server/server_payments_test.go, server/smtp_server_test.go, and web/src/components/PublishDialog.jsx. Oversized files such as server/server.go (≈14 k lines) and web/src/app/emojis.js concentrate many responsibilities, increasing cognitive load.

How To Use It

# Clone the repo
git clone https://github.com/moses-y/ntfy
cd ntfy

# Build and run the full stack with Docker
docker compose up -d          # uses docker-compose.yml

# Or run the server directly (requires Go 1.22+)
go run ./cmd/serve_windows.go   # entry point Execute → Run → handle

# Send a test notification from the CLI
go run ./cmd/publish.go \
    --topic test \
    --message "hello from ntfy"

Configuration lives in client/client.yml (default server URL, auth token) and can be overridden via command‑line flags documented in cmd/publish.go. The Dockerfile builds a minimal Alpine image; the Makefile provides make build and make test targets.

Real‑World Use

A CI pipeline can publish build status:

- name: Notify on failure
  if: failure()
  run: |
    go run ./cmd/publish.go \
      --topic ci-failure \
      --message "Build ${{ github.run_id }} failed"

The message appears instantly on any device running the ntfy Android/iOS app, without exposing credentials to the CI runner.

Code Health & Issues

Measured findings (static analysis)

  • Medium – Hub moduleweb/src/app/utils.js, web/src/components/routes.js, web/src/app/Session.js are imported by 23 modules; keep them small.
  • High – Deep nesting – 11 files (e.g., server/server_payments_test.go) have indentation depth 9; refactor to early returns.
  • High – Duplicated code – 264 identical 6‑line blocks across 53 test files; extract shared helpers.
  • High – Oversized fileserver/server.go (~14 k lines) and web/src/app/emojis.js are too large; split by responsibility.
  • Medium – High branching densityaction/action.go, ban/weights.go, cmd/config_loader.go contain 74 branches over 245 lines; consider strategy tables.
  • Low – TODO/FIXME – 3 markers in client/client.go; triage or resolve.

Repository hygiene findings

  • High – Workflow docs.yaml pushes directly to main; replace with PR‑based merges.
  • Medium – GitHub Actions lack explicit permissions for GITHUB_TOKEN.
  • Medium – Dockerfile uses mutable alpine tag; pin to digest.
  • Medium – No dependency‑vulnerability scan in CI; add dependency-review-action.
  • Medium – Container runs as root; add a non‑root USER.
  • Low – Jobs have no timeout-minutes; set reasonable limits.
  • Low – Missing convention files (.editorconfig, formatter config).

The Bottom Line

ntfy delivers a functional, self‑hosted notification broker with a mature Go backend, a React UI, and a ready‑to‑use CLI client. The codebase is test‑rich but suffers from several maintainability hot spots—large files, deep nesting, and duplicated test logic—that will increase effort for future changes. It is suitable for teams comfortable with Go and Docker who need an inexpensive, on‑premise push‑notification solution.