API VEGA

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 через Postgres NOTIFY

Движок доставки

  • Приоритеты — полосы 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 · /streamInbox внутри приложения, 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/mcpMCP-сервер (Streamable HTTP)
GET/v1/metrics/prometheusЭкспозиция для Prometheus
GET/u/:tokenСтраница однокликовой отписки

→ Все эндпоинты с примерами: docs/API.md, либо скормите docs/llms.txt вашему агенту.


Сравнение с альтернативами

NovuKnock / Courier / SuprSendnotifyd
Инфраструктура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-дашборддашборд + APIdigest-эндпоинт, MCP-сервер, Agent Skills, Prometheus
RealtimeWebSocketWebSocketSSE, нативный 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_PROVIDERresend (по умолчанию, если задан RESEND_API_KEY), cloudflare, smtp, agentmail, log
EMAIL_FROM, EMAIL_FROM_NAMEОтправитель по умолчанию для инстанса; проекты могут переопределить
EMAIL_FALLBACK_PROVIDERВторой провайдер при 429 / 5xx
EMAIL_RATE_PER_SECТемп исходящей отправки на реплику
SMS_PROVIDER, SMS_FROMtelnyx или 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.txtAPI в виде обычного текста для агентов

Участие в разработке

Приветствуются 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.

Написано на 🦀 в Гренобле, Франция 🏔️