notifyd
Отправляйте email, SMS, WhatsApp, push и in-app уведомления одним вызовом API.
Один бинарник на Rust размером 12 МБ, только PostgreSQL. Очереди, повторные попытки, переключение между провайдерами и тихие часы уже реализованы, а AI-агент может управлять сервером через MCP.
Быстрый старт • Клиенты • Примеры • Операции агента • Справочник API • Архитектура • Бенчмарки • llms.txt • Участие в разработке
Что это такое
Вашему приложению нужно сообщать людям о событиях: сброс пароля, отправка посылки, рассылка, красный значок в углу. notifyd — это небольшой сервер, который берёт всё это на себя, поэтому ваш код делает один вызов и больше не думает о провайдерах, ограничениях скорости, повторных попытках и часовых поясах.
┌───────────────────────────── notifyd ──────────────────────────────┐
your app ─────▶ │ POST /v1/send ─▶ queue ─▶ priority ─▶ pacing ─▶ retry ─▶ failover │ ─▶ email · sms · whatsapp
│ │ push · in-app · telegram · slack · discord
your agent ───▶ │ POST /mcp ─▶ digest · jobs · retry · suppressions · settings │
└───────────────────────── PostgreSQL only ──────────────────────────┘
-
Маленький и быстрый. Один бинарник размером 12 МБ, образ для загрузки 14 МБ, 13 МБ RAM в простое. Он принимает 44 000 уведомлений в секунду и обрабатывает 3 500 в секунду на ноутбуке (методика). Без Redis, без брокера сообщений, без хостинга дашборда: PostgreSQL — единственная зависимость.
-
Ничего не теряется. Сброс пароля всегда уходит раньше рассылки. Когда провайдер просит «сбавить темп», этот канал приостанавливается ровно на запрошенное время и возобновляется в порядке приоритета; при сбое его подхватывает второй провайдер. Повторные попытки, идемпотентность и тихие часы в часовом поясе каждого получателя встроены.
-
Ваши провайдеры, ваши данные. Resend, Cloudflare Email, любой SMTP, AgentMail, Telnyx, Twilio, APNs, Web Push, FCM. Self-hosted, MIT.
-
Управляется через API или AI-агента. Без админ-панели: эндпоинт дайджеста сообщает, что требует внимания и что делать, а те же операции доступны как инструменты MCP, поэтому дежурным может быть агент.
Сегодня в продакшене работают три инстанса, по одному на компанию, управляемые таким образом.
60-секундное объяснение, вымышленные данные. MP4 · Исходники Remotion
Отправка одним вызовом
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, curl работает; есть клиенты для TypeScript и Python.
Повторные попытки безопасны (idempotency_key), планирование — это поле
(scheduled_at), маркетинговая кампания — это POST /v1/batch с тысячами
подписчиков за вызов, и она попадает в массовую очередь, поэтому никогда не задерживает
сброс пароля. После любой отправки используйте GET /v1/jobs/:id. Запускаемые примеры
на всех языках: examples/.
Запуск из терминала
Тот же бинарник является CLI оператора и работает с любым инстансом (на хосте сервера
в окружении есть ADMIN_API_KEY, поэтому там он просто работает):
# provider, attempts, delivery events
notifyd retry # re-queue after fixing the cause notifyd send-test --project myapp --channel email --to you@example.com NOTIFYD_URL=https://notifyd.example.com NOTIFYD_ADMIN_API_KEY=… notifyd digest # from your laptop">
notifyd digest # what needs attention, with the action for each finding
notifyd jobs --status failed # --project, --channel, --topic, --recipient, --since 24h, --limit, --json
notifyd job <id> # provider, attempts, delivery events
notifyd retry <id> # re-queue after fixing the cause
notifyd send-test --project myapp --channel email --to you@example.com
NOTIFYD_URL=https://notifyd.example.com NOTIFYD_ADMIN_API_KEY=… notifyd digest # from your laptop
Позвольте агенту управлять этим
Большинство инструментов уведомлений созданы для человека, который кликает по дашборду. notifyd предоставляет работу оператора в виде инструментов с детализацией, которая нужна человеку-оператору:
1. Один вызов показывает, что требует внимания. GET /v1/admin/digest ранжирует
находки и указывает действие для каждой, в формате JSON или 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. Те же операции в виде инструментов MCP. POST /mcp — это Streamable HTTP
MCP-сервер (актуальная ревизия спецификации, устаревший initialize сохранён). Добавьте его в Claude
Code, Claude Desktop, Cursor или собственного агента:
{ "mcpServers": { "notifyd": {
"type": "http", "url": "https://notifyd.example.com/mcp",
"headers": { "Authorization": "Bearer ${NOTIFYD_ADMIN_API_KEY}" } } } }
| Инструмент | Что может делать агент |
|---|---|
digest | Ранжированные находки с действиями, очередью, результатами, задержкой, доставляемостью, по проектам |
list_jobs, get_job | Фильтрация по проекту, каналу, статусу, получателю, времени; просмотр провайдера, попыток, последней ошибки |
retry_job, cancel_job | Действия с зависшей или ошибочной отправкой |
list_projects, update_project | Идентичность отправителя (from_email, from_name), каналы, входящий лимит скорости, массовое send_window в часовом поясе получателей |
list_suppressions, add_suppression, release_suppression | Список подавлений с областью all или marketing |
template_metrics | Отправлено, не удалось, возвращено, открыто по каждому шаблону |
send_test | Проверить пайплайн от начала до конца на любом канале |
Каждый инструмент содержит аннотации readOnlyHint / destructiveHint и
outputSchema. Ключ оператора только для чтения (READONLY_API_KEY) открывает только
инструменты чтения — для агента, который отчитывается, но не должен действовать. Каждый вызов MCP
аудируется.
3. Дайджест приходит к вам. DIGEST_NOTIFY=telegram: (или
webhook Slack / Discord), и находки выше приходят в ваш чат, когда
что-то требует внимания; notifyd digest --to telegram:… отправляет дайджест прямо сейчас.
4. Всё, что нужно агенту для интеграции, есть в репозитории. docs/llms.txt
— это весь API простым текстом для контекстного окна; три Agent Skills
поставляются в skills/ (notifyd-operate, notifyd-integrate,
notifyd-deploy):
npx skills add rmzlb/notifyd
Опубликовано в официальном реестре MCP как mcp-name: io.github.rmzlb/notifyd
(server.json). Полное руководство оператора: docs/AGENT.md.
Возможности
Каналы
-
Email — Resend, Cloudflare Email Service, AgentMail, любой SMTP (
lettre), вложения, собственный отправитель для каждого проекта -
SMS — Telnyx или Twilio, переключается одной переменной
-
WhatsApp — Telnyx
-
Push — APNs нативно (аутентификация по токену
.p8, HTTP/2, badge, sound, thread id, silent push, отбрасывание мёртвых токенов), Web Push (VAPID), FCM -
In-app inbox — REST + realtime SSE (
EventSource), чтение / архивирование / избранное, badge непрочитанных, multi-replica через PostgresNOTIFY
Движок доставки
-
Приоритеты — полосы
critical,normal,bulk;/v1/batchи теги кампаний попадают вbulk -
Темп — token bucket на каждый канал (
EMAIL_RATE_PER_SEC); 429 от провайдера приостанавливает этот канал наRetry-Afterбез расходования попытки, остальные каналы продолжают работу, а при возобновлении порядок захвата отдаёт приоритетcritical -
Повторы — 30 с → 2 мин → 10 мин → 30 мин → 2 ч с jitter, 4xx завершаются сразу ошибкой, отклонённые батчи разбираются поэлементно
-
Failover — второй email-провайдер с circuit breaker (
EMAIL_FALLBACK_PROVIDER) -
Окна отправки — тихие часы на каждый проект, вычисляемые в часовом поясе каждого подписчика
-
Сегменты —
batchпо условию вроде «plan = pro and country in FR, BE» с небольшим набором фильтров, компилируемых в параметризованный SQL; сначала можно посчитать количество -
Планирование, идемпотентность, сборщик зависших задач, идемпотентность батчей
Управление
-
Отписка — одноклик по RFC 8058
List-Unsubscribeв каждом маркетинговом письме, области подавленияall/marketing -
Темы и предпочтения —
topicпри любой отправке («tips», «billing»); подписчики отказываются от отдельных тем и каналов, и это учитывается при постановке в очередь; страница отписки предлагает «только эту тему» прежде, чем «все маркетинговые» -
Мультипроектность — один инстанс, много проектов, изоляция по API-ключу, ротация ключей с периодом перекрытия
-
Маскирование PII в логах, audit log каждой мутации, rate limit на проект
Эксплуатация
-
Digest, MCP-сервер, Agent Skills,
llms.txt -
Метрики —
/v1/metrics,/v1/metrics/prometheus, метрики по шаблонам -
Отслеживание открытий и кликов — собственный пиксель и подписанные ссылки-редиректы для любого email-провайдера, по умолчанию в маркетинговых письмах (транзакционные ссылки не трогаются),
opened_at/clicked_atу задачи, отключается по проекту или по запросу -
Вебхуки — события доставки на ваши эндпоинты
-
Workflows — многошаговые последовательности, запускаемые событиями, состояние в Postgres
-
Шаблоны — подстановка
{{variable}}, хранятся по проекту
Быстрый старт
Docker (рекомендуется)
Образ читает конфигурацию из переменных окружения; монтировать файл конфигурации не нужно.
.env
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
Создайте проект и получите его 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_…", …}}
Готовый образ для linux/amd64 и linux/arm64: ghcr.io/rmzlb/notifyd.
Бинарник, crate, Nix или сборка из исходников
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
Релизные бинарники: Linux x86_64 и aarch64, macOS Intel и Apple Silicon.
NixOS: services.notifyd.enable = true; с environmentFile (см. flake.nix).
Ещё не выбрали провайдера? EMAIL_PROVIDER=log печатает письма вместо отправки.
Проверка
curl http://localhost:3400/v1/health
# → {"status":"ok","db":"ok","version":"0.2.2","commit":"…","uptime_seconds":12}
→ Полная настройка, вариант с TOML, заметки по продакшену: docs/SETUP.md
API кратко
Эндпоинты проекта принимают X-Api-Key: sk__…; операторские эндпоинты принимают admin-ключ (или read-only), передаваемый как X-Api-Key или Authorization: Bearer.
Эндпоинты inbox также принимают subscriber JWT.
| Метод | Эндпоинт | Что делает |
|---|---|---|
POST | /v1/send | Отправка по одному или нескольким каналам |
POST | /v1/batch | Отправка по списку подписчиков или по фильтру segment (полоса bulk, идемпотентно) |
POST | /v1/segments/preview | Подсчёт и выборка подписчиков, попадающих в сегмент |
GET | /v1/jobs/:id | Статус, провайдер, попытки, последняя ошибка |
GET | /v1/inbox/:id · /stream | Inbox внутри приложения, realtime-поток SSE |
POST | /v1/workflows/trigger | Запуск workflow по событию |
GET | /v1/admin/digest | Что требует внимания, с действиями |
GET | /v1/admin/jobs · /:id · POST …/:id/retry · /admin/send-test | Обзор и действия оператора (а также транспорт для CLI) |
PATCH | /v1/admin/projects/:id | Отправитель, каналы, rate limit, окно отправки |
POST | /mcp | MCP-сервер (Streamable HTTP) |
GET | /v1/metrics/prometheus | Экспозиция для Prometheus |
GET | /u/:token | Страница однокликовой отписки |
→ Все эндпоинты с примерами: docs/API.md, либо скормите docs/llms.txt вашему агенту.
Сравнение с альтернативами
| Novu | Knock / Courier / SuprSend | notifyd | |
|---|---|---|---|
| Инфраструктура | MongoDB + Redis + 4 контейнера приложения | Хостинг SaaS | Только Postgres, один образ на 14 МБ |
| Настройка | 30+ мин | Регистрация + дашборд | docker compose up (2 мин) |
| Язык | Node.js (несколько сервисов) | Н/Д (хостинг) | Rust (один бинарник) |
| Память в простое | 1,1 ГБ на 6 контейнеров (измерено) | Н/Д | 13 МБ, 23 МБ при разгрузке 100k задач (методика) |
| Образов для загрузки | 1,4 ГБ | Н/Д | 14 МБ |
| Пропускная способность | — | ограничена квотой | 44k задач/с в очередь, 3,5k задач/с разгружается (бенчмарки) |
| 429 от провайдера | задача падает | управляется | канал приостановлен на Retry-After, попытка не израсходована, сначала пробуется failover-провайдер |
| Приоритеты / окна отправки | ❌ | ✅ | ✅ полосы critical → bulk, окна по часовому поясу подписчика |
| Интерфейс для оператора | React-дашборд | дашборд + API | digest-эндпоинт, MCP-сервер, Agent Skills, Prometheus |
| Realtime | WebSocket | WebSocket | SSE, нативный EventSource, multi-replica |
| Self-hosted | ✅ (тяжёлый) | ❌ | ✅ один контейнер на компанию |
| Стоимость | Free tier / платно | за уведомление | Бесплатно навсегда, MIT |
Все числа в этой таблице мы измерили сами; данные по Novu получены из её собственного community docker-compose, в состоянии простоя, на той же машине и тем же инструментом, что и наши. Методика, характеристики железа и предупреждение о возможной предвзятости — в docs/BENCHMARKS.md.
Клиенты
Оба клиента покрывают весь API (send, batch, jobs, subscribers, preferences, templates, workflows, suppressions, inbox) и выбрасывают типизированную ошибку при любом ответе с кодом, отличным от 2xx.
TypeScript / JavaScript — до выхода пакета notifyd-sdk на npm (ожидается на этой неделе) используйте npm i github:rmzlb/notifyd; имя пакета и импорт ниже не изменятся (Node 18+, браузеры, edge-рантаймы; без зависимостей)
import { createNotifydClient } from 'notifyd-sdk';
const notifyd = createNotifydClient({ url: process.env.NOTIFYD_URL!, apiKey: process.env.NOTIFYD_API_KEY! });
const { jobIds } = await notifyd.send({
channels: ['email', 'in_app'], subscriberId: 'user-42',
subject: 'Your order shipped', body: 'Hi {{first_name}}, parcel {{parcel}} is on its way.',
vars: { first_name: 'Alice', parcel: 'FR-2041' }, idempotencyKey: 'order-2041-shipped',
});
const job = await notifyd.getJob(jobIds[0]); // status, provider, attempts, delivery events
// Browser inbox: subscriber token from your backend, live updates over EventSource
const inbox = createNotifydClient({ url, subscriberToken });
const stream = await inbox.openInboxStream('user-42', { onMessage: (e) => {
const event = JSON.parse(e.data);
if (event.type === 'new_notification') showToast(event.notification);
if (event.type === 'count_update') updateBadge(event.unread_count);
} });
Python — до выхода notifyd-sdk на PyPI (ожидается на этой неделе) используйте pip install "git+https://github.com/rmzlb/notifyd.git#subdirectory=clients/python" (3.9+, sync и asyncio, единственная зависимость — httpx) — clients/python
from notifyd import Notifyd
nd = Notifyd(os.environ["NOTIFYD_URL"], api_key=os.environ["NOTIFYD_API_KEY"])
result = nd.send(channels=["email", "in_app"], subscriber_id="user-42",
subject="Your order shipped", body="Hi {{first_name}}, parcel {{parcel}} is on its way.",
vars={"first_name": "Alice", "parcel": "FR-2041"}, idempotency_key="order-2041-shipped")
print(nd.get_job(result["job_ids"][0])["status"])
Для любого другого языка: API — это плоский JSON поверх HTTP, а docs/llms.txt — весь контракт на одной странице.
Workflows
Многошаговые последовательности, запускаемые событием; состояние хранится в Postgres и переживает перезапуски. Шаги выполняются по порядку; условие позволяет перейти к шагу с заданным индексом.
curl -X POST http://localhost:3400/v1/workflows -H "X-Api-Key: sk_myapp_xxx" -d '{
"id": "welcome-series", "name": "Welcome series", "trigger_event": "user.signup",
"steps": [
{"type": "send", "channel": "email", "template": "welcome"},
{"type": "delay", "duration_secs": 86400},
{"type": "condition", "field": "payload.plan", "operator": "eq", "value": "pro", "on_true": 4},
{"type": "send", "channel": "email", "template": "nudge"}
]}'
curl -X POST http://localhost:3400/v1/workflows/trigger -H "X-Api-Key: sk_myapp_xxx" \
-d '{"event": "user.signup", "subscriber_id": "user-42", "payload": {"plan": "free"}}'
Типы шагов: send, delay, condition (по inbox.is_read или payload.) и digest (некоторое время собирает события, затем отправляет одно сообщение). Полный скрипт: examples/welcome-series.sh.
Конфигурация
Переменные окружения — основной интерфейс конфигурации (именно их используют образ и compose-файл); для локальной разработки принимается notifyd.toml.
Обязательные: DATABASE_URL, JWT_SECRET, ADMIN_API_KEY. Далее — один провайдер:
| Переменная | Назначение |
|---|---|
EMAIL_PROVIDER | resend (по умолчанию, если задан RESEND_API_KEY), cloudflare, smtp, agentmail, log |
EMAIL_FROM, EMAIL_FROM_NAME | Отправитель по умолчанию для инстанса; проекты могут переопределить |
EMAIL_FALLBACK_PROVIDER | Второй провайдер при 429 / 5xx |
EMAIL_RATE_PER_SEC | Темп исходящей отправки на реплику |
SMS_PROVIDER, SMS_FROM | telnyx или twilio с их учётными данными |
PUBLIC_URL | Базовый URL для ссылок отписки в один клик |
READONLY_API_KEY | Необязательный ключ оператора с доступом только на чтение |
→ Все переменные с разбивкой по провайдерам: docs/CONNECTORS.md и docker-compose.yml. Справочник по TOML: notifyd.toml.example.
Архитектура
┌──────────────────────── 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
~14 000 строк на Rust, без unsafe. Бинарник — 12 МБ (16 МБ в статической сборке musl внутри образа), образ — 14 МБ при скачивании и 31 МБ на диске; RSS — 13 МБ в простое и 23 МБ при обработке очереди из 100 000 задач. → docs/ARCHITECTURE.md
Статус
notifyd — версия 0.x, работающая в продакшене у трёх компаний. Ещё не готово: пакеты для Swift и Kotlin; дашборд, A/B-тестирование и входящая почта отсутствуют намеренно. Порядок и объём работ описаны в docs/ROADMAP.md. Обратно несовместимые изменения анонсируются в примечаниях к релизам; схема очереди мигрирует автоматически.
Документация
| 📦 Установка | Локальная разработка, Docker, production |
| 🧪 Примеры | curl, TypeScript, Python, кампания на 10 000 получателей, workflow, инбокс в браузере, конфигурация MCP |
| 🐍 Python-клиент · TypeScript-клиент | Клиенты с полным покрытием API, типизированные ошибки, контрактные тесты |
| 🔌 Справочник по API | Каждый эндпоинт с примерами на curl / TypeScript / Rust |
| 🤝 Работа агента | Дайджест, инструменты MCP, ключ только для чтения, как агент запускает собственный инстанс |
| 🔌 Коннекторы | Провайдеры, переменные окружения, как добавить свой |
| 🏗️ Архитектура | Очередь, приоритеты, темп отправки, SSE, коннекторы |
| 📈 Бенчмарки | Потребление ресурсов, пропускная способность, как воспроизвести |
| 🚀 Развёртывание | Один инстанс на компанию, runbook |
| 📝 Статьи | Очередь уведомлений на чистом PostgreSQL: чего не даёт SKIP LOCKED |
| 🗺️ Дорожная карта | Что будет дальше — по порядку, с оценкой объёма; что сознательно не планируется |
| 📣 Продвижение | Реестры и каналы запуска |
| 🤖 llms.txt | API в виде обычного текста для агентов |
Участие в разработке
Приветствуются issues и пул-реквесты. Начать проще всего с задач, помеченных good first issue, а крупные функции всегда начинаются с issue. Подробнее — в 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
Лицензия
MIT.
Написано на 🦀 в Гренобле, Франция 🏔️