notifyd
Serviço de notificações auto-hospedado (email, SMS, push, in-app) em um único binário Rust sobre Postgres; seu servidor MCP permite que um agente opere a instância: resumos, jobs, tentativas, supressões, envios de teste.
Documentação
notifyd
O serviço de notificações que seu agente pode enviar por meio do e run.
Email, SMS, WhatsApp, push, inbox no aplicativo. Um único binário Rust. Apenas Postgres. Sem dashboard: um endpoint de resumo e um servidor MCP.
Início Rápido • Operações do agente • Referência da API • Arquitetura • Benchmarks • llms.txt • Contribuindo
O que é
Todo produto precisa enviar emails, SMS e notificações no aplicativo. As opções usuais são um SaaS hospedado cobrado por notificação, ou uma pilha auto-hospedada com MongoDB, Redis e quatro contêineres atrás de um dashboard React.
notifyd é a terceira opção: um único binário de 10 MB com PostgreSQL como
única dependência, que envia pelos provedores que você já tem
(Resend, Cloudflare Email Service, qualquer SMTP, AgentMail, Telnyx, Twilio,
Web Push, FCM), com um motor de entrega real (prioridades, ritmo, novas tentativas com
Retry-After, failover de provedor, janelas de envio, cancelamento de inscrição com um clique) e
nenhuma interface de administração. Operá-lo é uma chamada de API ou uma ferramenta MCP, então a
pessoa de plantão pode ser um agente de IA.
your app / your agent ──POST /v1/send──▶ notifyd ──▶ email · sms · whatsapp · push · in-app (SSE)
your agent ──POST /mcp─────▶ notifyd ──▶ digest · jobs · retries · suppressions · settings
Três instâncias rodam em produção hoje, uma por empresa, operadas dessa forma.

Explicação de 60 s, dados inventados. MP4 · Fonte Remotion
Envie em uma chamada
curl -X POST https://notifyd.example.com/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"},
"priority": "normal",
"idempotency_key": "report-42-ready"
}'
REST simples, curl é o SDK. Novas tentativas são seguras (idempotency_key), agendamento
é um campo (scheduled_at), uma campanha de marketing é POST /v1/batch com
milhares de assinantes por chamada e ela cai na faixa de volume para nunca atrasar uma
redefinição de senha. Siga qualquer envio com GET /v1/jobs/:id.
Deixe seu agente operá-lo
A maioria das ferramentas de notificação foi projetada para um humano clicando em um dashboard. notifyd expõe o trabalho do operador como ferramentas, com o detalhe que um operador humano precisaria:
1. Uma chamada diz o que precisa de atenção. GET /v1/admin/digest classifica
descobertas e informa a ação para cada uma, em JSON ou Markdown:
# notifyd digest — last 1d
Instance: commit e14e6f3, up 3d, email resend (+ smtp fallback), sms telnyx
## Findings
- **warning** — Primary email provider `resend` is resting for 47s after refusing messages; `smtp` is delivering.
_Nothing lost. Check the primary provider's status page; if it repeats, lower EMAIL_RATE_PER_SEC or move the primary role to the other provider._
- **warning** — Bounce rate 5.3 % over the window (14 bounced / 263 delivered).
_Above 5 % providers throttle or suspend the sender. Clean the recipient list; suppressions are applied automatically._
- **warning** — 3 job(s) failed in the last 1d (0.1 % of terminal jobs). Top cause: 422 unverified sender domain.
_Inspect with list_jobs(status=failed); permanent errors need a fix on the caller side, then retry_job._
## Queue pending 0, retry 2, processing 0
## Outcomes email/resend 4 812 sent, 3 failed · in_app 1 203 sent
## Latency email p50 0.6s, p95 2.1s (scheduled → accepted by provider)
## Deliverability delivered 4 790, bounced 14, complained 0, unsubscribed 9
2. As mesmas operações como ferramentas MCP. POST /mcp é um servidor MCP
Streamable HTTP (revisão atual da especificação, initialize legado mantido). Adicione-o ao Claude
Code, Claude Desktop, Cursor ou seu próprio agente:
{ "mcpServers": { "notifyd": {
"type": "http", "url": "https://notifyd.example.com/mcp",
"headers": { "Authorization": "Bearer ${NOTIFYD_ADMIN_API_KEY}" } } } }
| Ferramenta | O que o agente pode fazer |
|---|---|
digest | Descobertas classificadas com ações, fila, resultados, latência, entregabilidade, por projeto |
list_jobs, get_job | Filtrar por projeto, canal, status, destinatário, tempo; ver provedor, tentativas, último erro |
retry_job, cancel_job | Agir sobre um envio travado ou incorreto |
list_projects, update_project | Identidade do remetente (from_email, from_name), canais, limite de taxa de entrada, send_window de volume no fuso horário dos destinatários |
list_suppressions, add_suppression, release_suppression | Lista de supressão com escopo all ou marketing |
template_metrics | Enviados, falhos, devolvidos, abertos por modelo |
send_test | Provar o pipeline de ponta a ponta em qualquer canal |
Cada ferramenta carrega anotações readOnlyHint / destructiveHint e um
outputSchema. Uma chave de operador somente leitura (READONLY_API_KEY) expõe apenas
as ferramentas de leitura, para um agente que relata mas não deve agir. Cada chamada MCP é
auditada.
3. Tudo o que um agente precisa para integrar está no repositório. docs/llms.txt
é toda a API em texto simples para uma janela de contexto; três Agent Skills
acompanham em skills/ (notifyd-operate, notifyd-integrate,
notifyd-deploy):
npx skills add rmzlb/notifyd
Publicado no registro oficial MCP como mcp-name: io.github.rmzlb/notifyd
(server.json). Guia completo do operador: docs/AGENT.md.
Recursos
Canais
- Email — Resend, Cloudflare Email Service, AgentMail, qualquer SMTP (
lettre), anexos, identidade de remetente por projeto - SMS — Telnyx ou Twilio, troque com uma variável
- WhatsApp — Telnyx
- Push — Web Push (VAPID) ou FCM
- Inbox no aplicativo — REST + SSE em tempo real (
EventSource), ler / arquivar / favoritar, selo de não lidos, múltiplas réplicas via PostgresNOTIFY
Motor de entrega
- Prioridades — faixas
critical,normal,bulk; tags/v1/batche de campanha caem embulk - Ritmo — token bucket por canal (
EMAIL_RATE_PER_SEC); um 429 do provedor pausa esse canal porRetry-Aftersem consumir uma tentativa, outros canais continuam fluindo, e quando retoma a ordem de reivindicação colocacriticalprimeiro - Novas tentativas — 30 s → 2 m → 10 m → 30 m → 2 h com jitter, falha rápida em 4xx, lotes rejeitados caem item por item
- Failover — segundo provedor de email com circuit breaker (
EMAIL_FALLBACK_PROVIDER) - Janelas de envio — horários de silêncio por projeto, avaliados no fuso horário de cada assinante
- Agendamento, idempotência, coletor de trabalhos travados, idempotência de lote
Governança
- Cancelamento de inscrição — RFC 8058
List-Unsubscribecom um clique em cada email de marketing, escopos de supressãoall/marketing - Preferências — opt-in / opt-out por assinante
- Multi-projeto — uma instância, muitos projetos, isolados por chave de API, rotação de chave com período de carência
- Mascaramento de PII em logs, log de auditoria de cada mutação, limite de taxa por projeto
Operações
- Resumo, servidor MCP, Agent Skills,
llms.txt - Métricas —
/v1/metrics,/v1/metrics/prometheus, métricas por modelo - Webhooks — eventos de entrega para seus endpoints
- Workflows — sequências de múltiplas etapas acionadas por eventos, estado no Postgres
- Modelos — substituição
{{variable}}, armazenados por projeto
Início Rápido
Docker (recomendado)
A imagem lê sua configuração de variáveis de ambiente; nenhum arquivo de configuração para montar.
git clone https://github.com/rmzlb/notifyd.git && cd notifyd
cat > .env <<'EOF'
JWT_SECRET=change-me-32-random-chars-minimum
ADMIN_API_KEY=change-me-32-random-chars-minimum
RESEND_API_KEY=re_xxx
EMAIL_FROM=notifications@yourdomain.com
EOF
docker compose up -d # notifyd + Postgres 16, http://localhost:3400
Crie um projeto e obtenha sua chave de API:
curl -s -X POST http://localhost:3400/v1/admin/projects \
-H "X-Api-Key: $ADMIN_API_KEY" -H "Content-Type: application/json" \
-d '{"id":"myapp","name":"My app","channels":["email","in_app"],"from_email":"hello@yourdomain.com"}'
# → {"project": {"id": "myapp", "api_key": "sk_myapp_…", …}}
Imagem pré-compilada, linux/amd64 e linux/arm64: ghcr.io/rmzlb/notifyd.
Binário, crate, Nix ou fonte
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rmzlb/notifyd/releases/latest/download/notifyd-installer.sh | sh
brew install rmzlb/tap/notifyd # macOS and Linux, Homebrew
cargo binstall notifyd # prebuilt from the GitHub release
cargo install notifyd # build from crates.io
nix run github:rmzlb/notifyd # flake: packages, devShell, NixOS module
# then
DATABASE_URL=postgres://notifyd:pass@localhost:5432/notifyd \
JWT_SECRET=… ADMIN_API_KEY=… RESEND_API_KEY=… EMAIL_FROM=… notifyd
Binários de lançamento: Linux x86_64 e aarch64, macOS Intel e Apple Silicon.
NixOS: services.notifyd.enable = true; com um environmentFile (veja
flake.nix).
Sem provedor ainda? EMAIL_PROVIDER=log imprime emails em vez de enviá-los.
Verificar
curl http://localhost:3400/v1/health
# → {"status":"ok","db":"ok","version":"0.2.2","commit":"…","uptime_seconds":12}
→ Configuração completa, alternativa TOML, notas de produção: docs/SETUP.md
API em resumo
Endpoints de projeto usam X-Api-Key: sk_<project>_…; endpoints de operador usam
a chave de administrador (ou somente leitura), como X-Api-Key ou Authorization: Bearer.
Endpoints de inbox também aceitam um JWT de assinante.
| Método | Endpoint | O que faz |
|---|---|---|
POST | /v1/send | Enviar em um ou vários canais |
POST | /v1/batch | Enviar para muitos assinantes (faixa de volume, idempotente) |
GET | /v1/jobs/:id | Status, provedor, tentativas, último erro |
GET | /v1/inbox/:id · /stream | Inbox no aplicativo, stream SSE em tempo real |
POST | /v1/workflows/trigger | Acionar um workflow baseado em eventos |
GET | /v1/admin/digest | O que precisa de atenção, com ações |
GET | /v1/admin/jobs · POST …/:id/retry | Visão e ações do operador |
PATCH | /v1/admin/projects/:id | Remetente, canais, limite de taxa, janela de envio |
POST | /mcp | Servidor MCP (Streamable HTTP) |
GET | /v1/metrics/prometheus | Exposição Prometheus |
GET | /u/:token | Página de cancelamento de inscrição com um clique |
→ Cada endpoint com exemplos: docs/API.md, ou alimente docs/llms.txt ao seu agente.
vs. as alternativas
| Novu | Knock / Courier / SuprSend | notifyd | |
|---|---|---|---|
| Infra | MongoDB + Redis + 4 contêineres | SaaS hospedado | Apenas Postgres, uma imagem de 42 MB |
| Configuração | 30+ min | Cadastro + dashboard | docker compose up (2 min) |
| Linguagem | Node.js (vários serviços) | N/A (hospedado) | Rust (binário único) |
| Memória | não medido por nós | N/A | 13 MB ocioso, 23 MB drenando 100k trabalhos (método) |
| Throughput | — | limitado por cota | 44k trabalhos/s enfileirados, 3.5k trabalhos/s drenados (benchmarks) |
| 429 do provedor | trabalho falha | gerenciado | canal pausado por Retry-After, tentativa não consumida, provedor de failover tentado primeiro |
| Prioridades / janelas de envio | ❌ | ✅ | ✅ faixas crítico → volume, janelas por fuso horário do assinante |
| Superfície de operações | dashboard React | dashboard + API | endpoint de resumo, servidor MCP, Agent Skills, Prometheus |
| Tempo real | WebSocket | WebSocket | SSE, EventSource nativo, múltiplas réplicas |
| Auto-hospedado | ✅ (pesado) | ❌ | ✅ um contêiner por empresa |
| Custo | Nível gratuito / pago | por notificação | Gratuito para sempre, MIT |
Só publicamos números que medimos no próprio notifyd; o resto da tabela descreve formato, não desempenho. Método, hardware e aviso de viés em docs/BENCHMARKS.md.
Inbox no aplicativo e SDK TypeScript
import { createNotifydClient } from 'notifyd-sdk'; // pnpm add notifyd-sdk@github:rmzlb/notifyd
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' } });
// Browser: subscriber token from your backend, then a plain EventSource
const events = new EventSource(`${url}/v1/inbox/${userId}/stream?token=${jwt}`);
events.onmessage = (e) => { const d = JSON.parse(e.data);
if (d.type === 'new_notification') showToast(d.notification);
if (d.type === 'count_update') updateBadge(d.unread_count); };
Workflows
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": "condition", "check": "completed_onboarding",
"if_false": [{"type": "send", "channel": "email", "template": "nudge"}]}
]}'
O estado vive no Postgres e sobrevive a reinicializações.
Configuração
Variáveis de ambiente são a interface principal (é o que a imagem e o
arquivo compose usam); um notifyd.toml é aceito para desenvolvimento local.
Obrigatório: DATABASE_URL, JWT_SECRET, ADMIN_API_KEY. Depois um provedor:
| Variável | Propósito |
|---|---|
EMAIL_PROVIDER | resend (padrão quando RESEND_API_KEY está definido), cloudflare, smtp, agentmail, log |
EMAIL_FROM, EMAIL_FROM_NAME | Remetente padrão da instância; projetos podem sobrescrever |
EMAIL_FALLBACK_PROVIDER | Segundo provedor em 429 / 5xx |
EMAIL_RATE_PER_SEC | Ritmo de saída por réplica |
SMS_PROVIDER, SMS_FROM | telnyx ou twilio com suas credenciais |
PUBLIC_URL | URL base para links de cancelamento de inscrição com um clique |
READONLY_API_KEY | Chave de operador somente leitura opcional |
→ Cada variável, por provedor: docs/CONNECTORS.md e docker-compose.yml. Referência TOML: notifyd.toml.example.
Arquitetura
┌──────────────────────── notifyd (one binary) ───────────────────────┐
HTTP /v1, /mcp ─▶ axum API ─▶ jobs table ─▶ worker: claim (SKIP LOCKED, by priority) │
│ ├─ pacer per channel, channel pause on 429│
│ ├─ connectors (email/sms/whatsapp/push/in-app)
│ ├─ failover breaker, retries, reaper │
│ └─ webhooks, metrics, audit │
│ SSE hub ◀── Postgres NOTIFY ── (any replica) │
└───────────────────────────────┬──────────────────────────────────────┘
▼
PostgreSQL 16
src/
├── api/ # routes: send, batch, jobs, inbox, subscribers, templates, workflows, webhooks, admin ops, health
├── connectors/ # email (resend, cloudflare, smtp, agentmail, log), sms, whatsapp, push, in_app
├── worker.rs # claim by priority, batch context, finalize, retries, failover
├── pacing.rs # token buckets and channel pauses
├── failover.rs # provider circuit breaker
├── ops.rs # digest, findings, operator actions
├── mcp.rs # MCP server (tools, annotations, audit)
├── send_window.rs # quiet hours in the subscriber's timezone
├── unsubscribe.rs # List-Unsubscribe tokens and landing
├── sse.rs # inbox stream, Postgres NOTIFY fan-out
├── workflow_engine.rs, templates.rs, webhooks.rs, deliverability.rs, metrics.rs, pii.rs, middleware.rs
migrations/ # SQL, applied at start-up
skills/ # Agent Skills: operate, integrate, deploy
server.json # MCP registry entry
flake.nix # Nix package, devShell, NixOS module
dist-workspace.toml # cargo-dist: release binaries and installer
~12 000 linhas de Rust, sem unsafe. Binário de 10.8 MB, imagem de 42 MB, 13 MB RSS
ocioso, 23 MB ao drenar 100 000 trabalhos. → docs/ARCHITECTURE.md
Status
notifyd é um 0.x usado em produção por seus autores. O que ainda não faz: sem dashboard (por design), sem teste A/B, sem APNs, sem análise de email de entrada, sem cobrança multi-tenant. Mudanças que quebram são anunciadas nas notas de lançamento; o esquema da fila é migrado automaticamente.
Documentação
| 📦 Setup | Desenvolvimento local, Docker, produção |
| 🔌 Referência da API | Cada endpoint com exemplos em curl / TypeScript / Rust |
| 🤝 Operações de agente | Digest, ferramentas MCP, chave somente leitura, como um agente executa uma instância |
| 🔌 Conectores | Provedores, variáveis de ambiente, como adicionar um |
| 🏗️ Arquitetura | Fila, prioridades, ritmo, SSE, conectores |
| 📈 Benchmarks | Pegada, throughput, como reproduzir |
| 🚀 Implantações | Uma instância por empresa, runbook |
| 📝 Escrita | Uma fila de notificações apenas com PostgreSQL: o que SKIP LOCKED não oferece |
| 📣 Visibilidade | Registros e canais de lançamento |
| 🤖 llms.txt | A API em texto simples para agentes |
Contribuindo
Issues e pull requests são bem-vindos; good first issue é o lugar para
começar, e grandes funcionalidades começam com uma issue. Leia CONTRIBUTING.md.
git clone https://github.com/YOUR_USERNAME/notifyd.git && cd notifyd
cargo test && EMAIL_PROVIDER=log DATABASE_URL=… JWT_SECRET=dev ADMIN_API_KEY=dev cargo run
Licença
MIT.
Construído com 🦀 em Grenoble, França 🏔️