Skip to main content
Glama
richardfcampos

notify-hub

notify-hub

License: MIT Node.js >= 20 TypeScript Docker Compose MCP PRs Welcome

Self-hosted, free, multi-channel notification gateway. POST one message with a token and it fans out -- asynchronously, via a durable queue -- to every channel you've enabled: ntfy, Telegram, Email, Slack, Discord, WhatsApp, or a generic webhook. Built so a Claude Code hook can push you "task started" / "task finished" / "Claude needs you" notifications across every project, without ever blocking Claude.

Why

Claude Code (or any long-running script) leaves you watching a terminal for minutes with no way to know when it's done or needs input. notify-hub is a tiny, 100% free, self-hosted service you run once (docker compose up) that turns "send me a push" into a one-line HTTP call, decoupled from delivery by a Redis-backed queue with retries and per-channel dead-lettering.

Related MCP server: Trello Universal MCP Server

Architecture

client (curl / hook script)
  -> POST /notify (Bearer token)                [Fastify API]
  -> enqueue dispatch job                        [Redis / BullMQ]
  -> dispatch worker resolves channel set
  -> one delivery job per channel                [Redis / BullMQ]
  -> delivery worker sends via the channel's adapter
     (ntfy / telegram / email / slack / discord / whatsapp / webhook)
  • API only validates + enqueues; it never sends inline, so a slow/broken channel can't make /notify hang.

  • Worker does the actual fan-out and delivery; each channel gets its own job so retries/failures are isolated per channel (one channel down doesn't block the others).

  • Channels are pluggable: each one implements the same tiny interface (send(notification)); enabling one is a config toggle + credentials, and adding a brand-new one is a small adapter file (see the generic webhook adapter as the reference example).

Quickstart

git clone https://github.com/richardfcampos/notify-hub.git && cd notify-hub
./scripts/setup-env.sh   # guided setup: prompts for each channel's credentials
                         # (hidden input), generates your gateway token, writes
                         # .env with chmod 600. Or do it by hand:
                         #   cp .env.example .env && $EDITOR .env
docker compose up -d --build
curl http://localhost:8080/health
# => {"status":"ok","redis":true}

Send a notification:

curl -X POST http://localhost:8080/notify \
  -H "Authorization: Bearer <your-token-from-TOKENS>" \
  -H "Content-Type: application/json" \
  -d '{"title":"notify-hub","message":"hello from notify-hub"}'
# => 202 {"jobId":"..."}

POST /notify accepts:

Field

Required

Notes

message

yes

non-empty string

title

no

defaults to "Notification"

priority

no

one of low, default, high, urgent

tags

no

string array, passed through to channels that support it (e.g. ntfy)

channels

no

subset of your enabled channels to target this send to; omit to use the token's default channels

metadata

no

free-form object, passed through to channel adapters (e.g. the webhook channel)

Responses: 202 {jobId} (enqueued) · 400 (invalid body / unknown channel name) · 401 (missing/unknown token) · 503 (queue unreachable, so the caller never hangs).

Configuration

All config is env vars (see .env.example for every key). Key ones:

  • PORT -- API listen port (compose maps ${PORT:-8080} on the host).

  • TOKENS -- ;-separated name:token:defaultChannel1,defaultChannel2 entries. Example: phone:supersecrettoken:ntfy,telegram;desktop:othertoken:discord.

  • CHANNELS_ENABLED -- comma-separated list of channels to activate, e.g. ntfy,telegram,discord. A channel not listed here is never attempted, even if a request asks for it. A listed channel missing its required credentials makes the service refuse to start, naming the channel and the missing key -- so misconfiguration is caught immediately, not as a silent drop later.

Channels

Each row is the env keys a channel needs once it's in CHANNELS_ENABLED.

Channel

Env keys

Setup notes

ntfy

NTFY_URL, NTFY_TOPIC

Use https://ntfy.sh (public) or your own self-hosted ntfy server; subscribe to the topic in the ntfy app

telegram

TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID

Create a bot via @BotFather; get your chat id by messaging the bot then hitting getUpdates

email

SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_TO

Any SMTP provider (Gmail app password, SendGrid, etc.)

slack

SLACK_WEBHOOK_URL

Slack app -> Incoming Webhooks -> add to a channel

discord

DISCORD_WEBHOOK_URL

Server channel settings -> Integrations -> Webhooks

whatsapp

WHATSAPP_PHONE, WHATSAPP_APIKEY

Free personal API via CallMeBot -- message their bot to activate, rate-limited

webhook

WEBHOOK_URL

Reference extensibility adapter: POSTs the full notification JSON to any URL you control (Gotify, a custom listener, etc.)

Adding a brand-new channel: implement the NotificationChannel interface (one send() method) in src/channels/adapters/, export a ChannelRegistryEntry (factory + required env keys), and add one line to src/channels/channel-registry.ts. No other core changes needed.

Claude Code hook

A zero-dependency hook script pushes "start" / "end" / "needs-input" events to notify-hub globally, across every Claude Code project. See clients/claude-code/install.md for the full setup (settings.json snippet + env vars).

MCP server

notify-hub also ships as an MCP (Model Context Protocol) server over stdio, exposing three tools -- send_notification, list_channels, check_gateway_health -- so an agent (Claude Code, Claude Desktop, any MCP client) can push notifications and check the gateway as tool calls instead of hand-rolled HTTP. It's a thin client of the already-running gateway (no direct Redis access). See clients/mcp/install.md for the full setup (claude mcp add command + generic mcpServers JSON config).

Admin panel

A local, dark-themed dashboard for managing everything above without hand- editing .env:

  • View every channel (ntfy, Telegram, Email, Slack, Discord, WhatsApp, webhook), toggle it on/off, and edit its credentials -- masked by default, revealed with one click.

  • Manage token profiles (TOKENS): add/remove, edit the token, and pick each profile's default channels.

  • Save & Apply validates, backs up .env (timestamped), writes the new file, and runs docker compose up -d --no-build api worker -- one click, no terminal.

  • Send test per channel posts a real notification and shows the actual delivery outcome (✅ sent, or the real failure reason ❌), not just "enqueued".

  • Live gateway status (health, redis, active channels) and a tail of recent worker deliveries.

Comes up automatically as part of the stack -- no extra step:

docker compose up -d
# => http://127.0.0.1:8081

npm run admin still works as a host-side dev alternative (no Docker rebuild needed while iterating on the panel itself):

npm run admin
# => admin panel: http://127.0.0.1:8081

Reachability: in compose the host-side bind defaults to 0.0.0.0 (like the other services on a typical homelab host), so the panel is reachable from your other devices -- e.g. over a Tailscale tailnet as http://<machine>:8081. The panel has no auth and displays secrets, so anyone who can reach the port can read and rewrite your config: keep that surface to networks you trust (a WireGuard/Tailscale tailnet is a good fit; an untrusted LAN is not). Set ADMIN_BIND=127.0.0.1 in .env to make it localhost-only. The explicit bind template is asserted by src/admin/compose-invariants.test.ts. The host-side dev mode (npm run admin) binds 127.0.0.1 by default regardless.

Docker-socket trade-off: the admin service mounts /var/run/docker.sock so Save & Apply can run docker compose up -d --no-build api worker against the real stack from inside the container (it never recreates the admin service itself -- that would kill the container mid-request). This gives the admin container control of the host's Docker daemon, the same pattern used by tools like Portainer. Accepted because the panel is a personal tool on a trusted network -- combined with the reachability note above, treat "who can open the panel" as "who can administer this Docker host".

Development

npm install
npm run build        # tsc -> dist/bin/{api,worker,admin}.js, copies
                      #   src/admin/ui -> dist/admin/ui (static UI assets)
npm run test          # full suite -- REQUIRES Docker (spins up redis:7-alpine
                      #   via testcontainers for the BullMQ retry/dead-letter
                      #   integration test); set REDIS_TEST_URL to reuse a
                      #   running Redis instead of a container
npm run test:unit     # no-Docker fast subset (src unit tests only)
npm run test:integration  # just the Redis-backed queue integration test
npm run dev:api        # tsx, no build step
npm run dev:worker

Verified

docker compose up -d --build was run end-to-end against a real ntfy.sh topic:

  • docker compose ps -- all 3 services (redis, api, worker) up, api healthcheck healthy.

  • curl http://localhost:8080/health -> 200 {"status":"ok","redis":true}.

  • curl -X POST http://localhost:8080/notify -H "Authorization: Bearer <token>" -d '{"title":"notify-hub","message":"smoke test"}' -> 202 {"jobId":"1"}.

  • Worker log: {"channel":"ntfy","msg":"sending notification"} followed by {"channel":"ntfy","msg":"notification sent"}.

  • Confirmed on the ntfy side too: curl "https://ntfy.sh/<topic>/json?poll=1" returned the exact message: {"title":"notify-hub","message":"smoke test", ...}.

If your environment can't reach the public internet (ntfy.sh), point NTFY_URL at a self-hosted ntfy instance instead and repeat the same smoke steps.

Contributing

Contributions are welcome — the most useful one is a new channel adapter. Each channel is a small self-contained file implementing one interface:

  1. Add src/channels/adapters/<name>-channel.ts implementing NotificationChannel (a single send(notification) method) plus its ChannelRegistryEntry (factory + required env keys).

  2. Register it with one line in src/channels/channel-registry.ts.

  3. Add unit tests next to it (happy path + error path, using the fakes in test/helpers/fakes.ts — no real network in tests).

  4. npm run test:unit must pass; open a PR.

Ideas: Gotify, Matrix, Pushover, Signal, Mattermost, Rocket.Chat, SMS gateways. Bug reports and docs fixes are equally appreciated — open an issue.

License

MIT © Richard Campos

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/richardfcampos/notify-hub'

If you have feedback or need assistance with the MCP directory API, please join our Discord server