Odel
notifyd

notifyd

@rmzlb1RustMITUpdated Today

Operate a self-hosted notifyd instance: digest, jobs, retries, suppressions, projects, test sends.

Server endpointStreamable HTTPProbe failed

This is the third-party server itself — Odel doesn't run it. Hitting this URL directly talks straight to the upstream server with no auth or proxying. Connect through Odel to front it with managed auth.

notifyd

notifyd

Agent-first notification service. One Rust binary. Postgres only. No Redis, no Mongo, no nonsense.

License Container image CI Rust Image size Memory MCP server Agent Skills

Quick StartAPI ReferenceSetup GuideArchitectureBenchmarksLLM DocsContributing


The Problem

Your AI agent needs to send an email. Or a push notification. Or update an in-app inbox.

You look at Novu: MongoDB, Redis, 4 containers, a React SDK, 30 minutes of setup. Your agent doesn't care about any of that. It just wants to POST /v1/send and move on.

notifyd is what that looks like. A single Rust binary. One POST call. Your agent sends notifications and gets back to work.

Agent ──POST /v1/send──→ notifyd ──→ Email (Resend)
                                 ──→ SMS (Twilio/Telnyx)
                                 ──→ Push (FCM)
                                 ──→ In-App (SSE)

Why Agents Love This

Most notification services were designed for humans clicking buttons in a dashboard. notifyd was designed for agents making API calls.

Flat REST API — no SDK needed, no WebSocket handshake, no complex auth flows. curl works. Your agent's HTTP client works.

docs/llms.txt — the entire API reference in plain text, optimized for LLM context windows. Point your agent at it and it can call any endpoint. (View it)

Idempotency built-in — agents retry. That's fine. Pass idempotency_key and notifyd deduplicates.

One binary, one config filedocker compose up and you have a notification service. No infra degree required.

# Your agent sends a notification. That's it.
curl -X POST http://localhost:3400/v1/send \
  -H "X-Api-Key: sk_myapp_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channels": ["email", "in_app"],
    "subscriber_id": "user-1",
    "subject": "Your report is ready",
    "body": "Hey {{first_name}}, the analysis you requested is complete.",
    "vars": {"first_name": "Alice"},
    "idempotency_key": "report-42-ready"
  }'

Connect Your Agent to the Docs

Feed docs/llms.txt to any LLM agent and it can operate the full API:

https://raw.githubusercontent.com/rmzlb/notifyd/main/docs/llms.txt

Or describe notifyd as a tool:

{
  "name": "send_notification",
  "description": "Send email/SMS/push/in-app via notifyd",
  "endpoint": "POST /v1/send",
  "auth": "X-Api-Key header"
}

vs. The Alternatives

NovuKnock / Courier / SuprSendnotifyd
InfraMongoDB + Redis + 4 containersHosted SaaSPostgres only, one 42 MB image
Setup30+ minSignup + dashboarddocker compose up (2 min)
LanguageNode.js (multiple services)N/A (hosted)Rust (single binary)
Memorynot measured by usN/A13 MB idle, 23 MB draining 100k jobs (method)
Throughputquota-bound44k jobs/s enqueued, 3.5k jobs/s drained (benchmarks)
Provider 429job failsmanagedlane paused, Retry-After honoured, failover provider
Priorities / send windows✅ critical → bulk lanes, per-subscriber timezone windows
Ops for agentsReact dashboarddashboard + APIGET /v1/admin/digest, MCP server, Agent Skills, Prometheus
RealtimeWebSocketWebSocketSSE (native EventSource, multi-replica via Postgres NOTIFY)
Self-hosted✅ (heavy)✅ (one container per company)
QueueRedis + BullMQManagedPostgres SKIP LOCKED, stuck-job reaper
CostFree tier / paidper notificationFree forever, MIT

Features

  • 📧 Email — Resend, Cloudflare Email Service, AgentMail or any SMTP, with an optional failover provider
  • 📱 SMS — Twilio or Telnyx (swap in config, zero code change)
  • 🔔 Push — FCM (Firebase Cloud Messaging)
  • 💬 In-app inbox — REST + realtime SSE stream
  • ⏰ Schedulingscheduled_at on any notification
  • 🔄 Retry — backoff 30s → 2m → 10m → 30m → 2h with jitter, provider Retry-After honoured
  • 🚥 Priorities — critical / normal / bulk lanes, a 429 pauses only the lane that hit it
  • 🕰️ Send windows — per-project quiet hours in each subscriber's timezone
  • 📭 UnsubscribeList-Unsubscribe one-click on every marketing email, suppression scopes
  • 🔑 Idempotency — safe agent retries
  • 📋 Templates{{variable}} substitution, stored per project
  • 🏢 Multi-project — one instance, many projects, isolated by API key
  • ⚡ Workflows — event-triggered multi-step sequences
  • 👤 Preferences — per-subscriber opt-in/opt-out
  • 🔍 Audit log — every mutation logged
  • 🚦 Rate limiting — per-project sliding window
  • 📊 Metrics/v1/metrics, /v1/metrics/prometheus, per-template metrics
  • 🧭 Digest + MCPGET /v1/admin/digest and POST /mcp so an agent operates the instance
  • 🪝 Webhooks — delivery events to your endpoints

Quick Start

Docker (recommended)

Prebuilt image (linux/amd64 + linux/arm64, 42 MB): ghcr.io/rmzlb/notifyd.

git clone https://github.com/rmzlb/notifyd.git && cd notifyd
cp notifyd.toml.example notifyd.toml
# Edit notifyd.toml — add your Resend API key at minimum

docker compose up -d
# → notifyd running on http://localhost:3400

From source

# Rust 1.75+, PostgreSQL 16+
git clone https://github.com/rmzlb/notifyd.git && cd notifyd
cp notifyd.toml.example notifyd.toml
cargo run

Verify

curl http://localhost:3400/v1/health
# → {"status":"ok","db":"ok","version":"0.2.0"}

→ Full setup: docs/SETUP.md


API at a Glance

Every endpoint uses X-Api-Key: sk_<project>_xxx. Inbox endpoints also accept subscriber JWT.

MethodEndpointWhat it does
POST/v1/sendSend notification (email, SMS, push, in-app)
POST/v1/batchSend to multiple subscribers
GET/v1/inbox/:idList in-app notifications
GET/v1/inbox/:id/streamSSE realtime stream
POST/v1/workflows/triggerTrigger event-based workflow
GET/v1/healthHealth check
GET/v1/metricsService metrics

→ Full reference: docs/API.md — or feed docs/llms.txt to your agent.


TypeScript SDK

A small official SDK now ships in this repo for backend + frontend apps.

pnpm add notifyd-sdk@github:rmzlb/notifyd
import { createNotifydClient } from 'notifyd-sdk';

const notifyd = createNotifydClient({
  url: process.env.NOTIFYD_URL!,
  apiKey: process.env.NOTIFYD_API_KEY!,
});

await notifyd.send({
  channels: ['email', 'in_app'],
  subscriberId: 'user-123',
  subject: 'Your report is ready',
  body: 'Hey {{first_name}}, the analysis is complete.',
  vars: { first_name: 'Alice' },
});

const token = await notifyd.createSubscriberToken({
  subscriberId: 'user-123',
  ttlHours: 8,
});

It wraps the REST API with typed helpers for send, subscribers, inbox, unread count, mark read, and SSE stream setup.


In-App Inbox

Complete notification inbox with realtime SSE. No WebSocket library, no Redis pub/sub — just native EventSource.

// Connect to realtime stream
const events = new EventSource(
  `https://notifyd.example.com/v1/inbox/${userId}/stream?token=${jwt}`
);

events.onmessage = (e) => {
  const data = JSON.parse(e.data);
  if (data.type === 'new_notification') showToast(data.notification);
  if (data.type === 'count_update') updateBadge(data.unread_count);
};

Features: read/unread, archive, todo/star, pagination, unread count badge, realtime push.


Workflow Engine

Multi-step notification sequences triggered by events:

curl -X POST http://localhost:3400/v1/workflows \
  -H "X-Api-Key: sk_myapp_xxx" \
  -d '{
    "id": "welcome-series",
    "trigger_event": "user.signup",
    "steps": [
      {"type": "send", "channel": "email", "template": "welcome"},
      {"type": "delay", "duration": "24h"},
      {"type": "send", "channel": "email", "template": "getting_started"},
      {"type": "delay", "duration": "72h"},
      {"type": "condition", "check": "completed_onboarding", "if_false": [
        {"type": "send", "channel": "email", "template": "nudge"}
      ]}
    ]
  }'

State persisted in Postgres — survives restarts. No in-memory state to lose.


Built for agents, not dashboards

No admin UI. GET /v1/admin/digest tells you, in one call, what deserves attention and what to do about it; POST /mcp exposes the same operations as MCP tools so Claude Code, Claude Desktop or Cursor can run the instance:

{ "mcpServers": { "notifyd": { "type": "http", "url": "https://notifyd.example.com/mcp",
  "headers": { "Authorization": "Bearer ${NOTIFYD_ADMIN_API_KEY}" } } } }

docs/AGENT.md

Agent Skills for Claude Code, Cursor and friends live in skills/: notifyd-operate, notifyd-integrate, notifyd-deploy.

npx skills add rmzlb/notifyd

MCP registry name: mcp-name: io.github.rmzlb/notifyd (see server.json).

Configuration

Single TOML file. Minimal setup:

[server]
port = 3400
jwt_secret = "your-secret-here"

[database]
url = "postgres://notifyd:pass@localhost:5432/notifyd"

[connectors.email]
provider = "resend"
api_key = "re_xxx"
from = "notifications@yourdomain.com"

[projects.myapp]
api_key = "sk_myapp_xxx"
channels = ["email", "in_app"]

→ Full config: notifyd.toml.example. Providers and their environment variables (Resend, Cloudflare Email Service, any SMTP, AgentMail, Telnyx, Twilio, web push): docs/CONNECTORS.md.


Project Structure

notifyd/
├── src/
│   ├── main.rs              # Server bootstrap, graceful shutdown
│   ├── config.rs             # TOML config
│   ├── db.rs                 # sqlx models
│   ├── worker.rs             # Background job processor
│   ├── workflow_engine.rs    # Event-driven workflows
│   ├── sse.rs                # SSE broadcaster (tokio channels)
│   ├── templates.rs          # {{var}} engine
│   ├── pii.rs                # PII masking for logs
│   ├── middleware.rs          # Rate limiter + audit
│   ├── api/                  # 13 route modules
│   └── connectors/           # Email, SMS, Push, In-App
├── migrations/               # SQL migrations (auto-run)
├── Dockerfile                # Multi-stage + cargo-chef
├── docker-compose.yml        # notifyd + Postgres
├── notifyd.toml.example      # Config reference
└── docs/                     # API ref, setup, architecture, llms.txt

~12,000 lines of Rust, no unsafe. 10.8 MB binary, 42 MB image, 13 MB RSS at idle.


Documentation

📦 Setup GuideLocal dev, Docker, production deploy
🔌 API ReferenceEvery endpoint with curl/TS/Rust examples
🏗️ ArchitectureQueue design, SSE internals, connectors
📈 BenchmarksFootprint, throughput, how to reproduce
📣 VisibilityRegistries, lists and launch channels, in order
🔌 ConnectorsProviders, environment variables, adding one
🤝 Agent operationsDigest, MCP tools, how an agent runs an instance
🚀 DeploymentsOne instance per company, runbook, inventory
🤖 LLM DocsFull API in plain text — feed to your agent

Contributing

notifyd is built in Grenoble, in the French Alps 🏔️ — but open to contributors from everywhere.

  1. Read the Contributing Guide
  2. Check open issuesgood first issue is a great start
  3. Big features → open an issue first
git clone https://github.com/YOUR_USERNAME/notifyd.git
cd notifyd && cp notifyd.toml.example notifyd.toml
cargo test && cargo run

License

MIT — use it however you want.


Built with 🦀 in Grenoble, France 🏔️