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
- Entry point – The HTTP server starts in
server/server.goviafunc handle(line 506). This handler is registered with the Go HTTP mux and is the only place that reaches 266 internal functions. - Authentication flow –
handlecallsmaybeAuthenticate, which invokesAuthAllowed→Tokens.Tokensperforms a database query (db.Query) defined indb/db.go. This is the primary outbound DB interaction (1 of 91 DB‑touching functions). - Message routing – After auth, the request is parsed and passed to
execServe(inserver/server.go).execServecreates aMessageobject and callsNew(41 calls) andParseDuration(12 calls) before pushing it into the in‑memory cache (createMessageCache). - 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) andclient/client.go(client config). - High‑impact hubs –
web/src/app/utils.jsandweb/src/components/routes.jsare imported by 23 modules each; changes here have a large blast radius. In the Go code,forEachBackendis called from 400 places, making it a critical change point. - Hot spots – Deep nesting (max depth 9) appears in
server/server_payments_test.go,server/smtp_server_test.go, andweb/src/components/PublishDialog.jsx. Oversized files such asserver/server.go(≈14 k lines) andweb/src/app/emojis.jsconcentrate 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 module –
web/src/app/utils.js,web/src/components/routes.js,web/src/app/Session.jsare 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 file –
server/server.go(~14 k lines) andweb/src/app/emojis.jsare too large; split by responsibility. - Medium – High branching density –
action/action.go,ban/weights.go,cmd/config_loader.gocontain 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.yamlpushes directly tomain; replace with PR‑based merges. - Medium – GitHub Actions lack explicit
permissionsforGITHUB_TOKEN. - Medium – Dockerfile uses mutable
alpinetag; 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.