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

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.

License Container image crates.io CI Image size Memory MCP registry Agent Skills

Início RápidoOperações do agenteReferência da APIArquiteturaBenchmarksllms.txtContribuindo


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.

60-second explainer: one send call, priority order under a provider 429, an agent operating the instance over MCP
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}" } } } }
FerramentaO que o agente pode fazer
digestDescobertas classificadas com ações, fila, resultados, latência, entregabilidade, por projeto
list_jobs, get_jobFiltrar por projeto, canal, status, destinatário, tempo; ver provedor, tentativas, último erro
retry_job, cancel_jobAgir sobre um envio travado ou incorreto
list_projects, update_projectIdentidade 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_suppressionLista de supressão com escopo all ou marketing
template_metricsEnviados, falhos, devolvidos, abertos por modelo
send_testProvar 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 Postgres NOTIFY

Motor de entrega

  • Prioridades — faixas critical, normal, bulk; tags /v1/batch e de campanha caem em bulk
  • Ritmo — token bucket por canal (EMAIL_RATE_PER_SEC); um 429 do provedor pausa esse canal por Retry-After sem consumir uma tentativa, outros canais continuam fluindo, e quando retoma a ordem de reivindicação coloca critical primeiro
  • 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-Unsubscribe com um clique em cada email de marketing, escopos de supressão all / 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étodoEndpointO que faz
POST/v1/sendEnviar em um ou vários canais
POST/v1/batchEnviar para muitos assinantes (faixa de volume, idempotente)
GET/v1/jobs/:idStatus, provedor, tentativas, último erro
GET/v1/inbox/:id · /streamInbox no aplicativo, stream SSE em tempo real
POST/v1/workflows/triggerAcionar um workflow baseado em eventos
GET/v1/admin/digestO que precisa de atenção, com ações
GET/v1/admin/jobs · POST …/:id/retryVisão e ações do operador
PATCH/v1/admin/projects/:idRemetente, canais, limite de taxa, janela de envio
POST/mcpServidor MCP (Streamable HTTP)
GET/v1/metrics/prometheusExposição Prometheus
GET/u/:tokenPá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

NovuKnock / Courier / SuprSendnotifyd
InfraMongoDB + Redis + 4 contêineresSaaS hospedadoApenas Postgres, uma imagem de 42 MB
Configuração30+ minCadastro + dashboarddocker compose up (2 min)
LinguagemNode.js (vários serviços)N/A (hospedado)Rust (binário único)
Memórianão medido por nósN/A13 MB ocioso, 23 MB drenando 100k trabalhos (método)
Throughputlimitado por cota44k trabalhos/s enfileirados, 3.5k trabalhos/s drenados (benchmarks)
429 do provedortrabalho falhagerenciadocanal 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çõesdashboard Reactdashboard + APIendpoint de resumo, servidor MCP, Agent Skills, Prometheus
Tempo realWebSocketWebSocketSSE, EventSource nativo, múltiplas réplicas
Auto-hospedado✅ (pesado)✅ um contêiner por empresa
CustoNível gratuito / pagopor notificaçãoGratuito 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ávelPropósito
EMAIL_PROVIDERresend (padrão quando RESEND_API_KEY está definido), cloudflare, smtp, agentmail, log
EMAIL_FROM, EMAIL_FROM_NAMERemetente padrão da instância; projetos podem sobrescrever
EMAIL_FALLBACK_PROVIDERSegundo provedor em 429 / 5xx
EMAIL_RATE_PER_SECRitmo de saída por réplica
SMS_PROVIDER, SMS_FROMtelnyx ou twilio com suas credenciais
PUBLIC_URLURL base para links de cancelamento de inscrição com um clique
READONLY_API_KEYChave 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

📦 SetupDesenvolvimento local, Docker, produção
🔌 Referência da APICada endpoint com exemplos em curl / TypeScript / Rust
🤝 Operações de agenteDigest, ferramentas MCP, chave somente leitura, como um agente executa uma instância
🔌 ConectoresProvedores, variáveis de ambiente, como adicionar um
🏗️ ArquiteturaFila, prioridades, ritmo, SSE, conectores
📈 BenchmarksPegada, throughput, como reproduzir
🚀 ImplantaçõesUma instância por empresa, runbook
📝 EscritaUma fila de notificações apenas com PostgreSQL: o que SKIP LOCKED não oferece
📣 VisibilidadeRegistros e canais de lançamento
🤖 llms.txtA 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 🏔️