Перейти к содержимому

WebSocket-протокол

Push-уведомления Notifly доставляются по WebSocket. Любой клиент (web-админка, Android-приложение, кастомный desktop) держит постоянное соединение по wss://, и каждое новое сообщение приходит фреймом сразу после того, как REST-эндпоинт POST /message его сохранил.

┌──────────┐ ┌────────────────────┐
│ Клиент │ wss://api/ws ───► │ Yandex API GW │
└─────┬────┘ └─────────┬──────────┘
│ │ CONNECT / MESSAGE / DISCONNECT
│ получает сообщения ▼
│ ┌────────────────────┐
│ │ notifly-ws-handler │ Cloud Function
│ │ (Go) │
│ └─────────┬──────────┘
│ │ S3 API
│ ▼
│ ┌────────────────────┐
│ │ Object Storage │ connections/<id>.json
│ │ notifly-ws-conns │
│ └────────────────────┘
│ ───────── после POST /message ───────────────┐
▼ ▼
┌──────────┐ ┌────────────────────┐
│ notifly │ push фрейм по @connections │ Yandex API GW │
│ -api │ ───────────────────────────► │ Management API │
└──────────┘ └────────────────────┘
  • Соединение хранится в S3 (notifly-ws-connections) как connections/<id>.json.
  • Авторизация — ?token=<clientToken> в query при CONNECT. Соединение ассоциируется с user_id и используется как адрес доставки push.
  • Доставка инициируется REST-функцией notifly-api: после INSERT messages она находит все соединения нужного пользователя и шлёт фрейм через Management-API API Gateway: POST /@connections/<id>.
wss://<домен>/ws?token=C<clientToken>

Доменом служит ваш API Gateway. Для облачной версии Notifly это https://api.notifly.ru/ws — но протокол устроен одинаково для любого self-hosted-стенда.

const ws = new WebSocket(`wss://api.notifly.ru/ws?token=${clientToken}`);
ws.onopen = () => {
console.log('Connected');
// (необязательно) keep-alive раз в 30 секунд
setInterval(() => ws.send(JSON.stringify({action: 'ping'})), 30_000);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.id && data.message) {
// это входящее уведомление
console.log('Notification:', data.title, data.message);
} else {
// служебный ответ (pong, status, …)
console.debug('WS:', data);
}
};
ws.onclose = () => console.log('Disconnected');
Окно терминала
npm install -g wscat
wscat -c "wss://api.notifly.ru/ws?token=Caw9...XYZ"
> {"action": "ping"}
< {"action":"pong","timestamp":"2026-04-30T10:11:12Z","connection_id":"c057..."}
> {"action": "status"}
< {"action":"status","connection_id":"c057...","connected_at":"...","source_ip":"..."}

Все фреймы — JSON.

actionОписание
pingKeep-alive / проверка связи
statusИнформация о текущем соединении
listСписок всех соединений пользователя
broadcastРассылка остальным соединениям пользователя
любоеЭхо обратно (для отладки)

Это «полезная нагрузка» — то, ради чего вообще держится сокет. Формат полностью совпадает с MessageExternal REST API:

{
"id": 1714476672123456789,
"appid": 12345,
"title": "Деплой завершён",
"message": "Сборка #874 ушла на прод.",
"priority": 5,
"extras": null,
"date": "2026-04-30T10:11:12Z"
}
{"action":"pong","timestamp":"2026-04-30T10:11:12Z","connection_id":"c057..."}
{
"action":"status",
"connection_id":"c057...",
"connected_at":"2026-04-30T10:11:12Z",
"source_ip":"95.104.77.29",
"last_activity":"2026-04-30T10:11:42Z"
}
{
"action":"connections",
"count":2,
"connections":[
{"connection_id":"c057...","connected_at":"...","status":"connected", ...}
]
}
{"action":"echo","message":{"action":"foo","data":42}}
  1. Клиент открывает wss://.../ws?token=....
  2. API Gateway → CONNECT → Cloud Function записывает ConnectionInfo (включая user_id) в connections/<id>.json в S3.
  3. Клиент шлёт фреймы → MESSAGE → функция отвечает в Response.Body.
  4. REST-функция notifly-api, создав новое сообщение, перечисляет соединения пользователя в S3 и шлёт фрейм через Management-API.
  5. Клиент закрывает сокет → DISCONNECT → функция удаляет JSON-файл соединения.

При CONNECT обязателен ?token=<token> в query-параметре. Поддерживаются два типа токенов:

Префикс / длинаТипЧто получает соединение
C... (23)Client-токенВсе сообщения пользователя — по всем его каналам.
A... (23)App-токенТолько сообщения конкретного канала (которому принадлежит токен).

Это значит, что получать сообщения может не только устройство-получатель. Любое приложение, сервис, скрипт или CI-раннер может подписаться на канал прямо по тому же app-токену, которым шлёт сообщения, — и обработать их программно в реальном времени.

Альтернатива — использовать обычный Authorization: Bearer ... в первом HTTP-запросе при CONNECT (не все клиенты умеют отдавать заголовки на WebSocket-handshake; query-параметр поддерживается всегда).

Подписка по app-токену канала (приложение-получатель)

Заголовок раздела «Подписка по app-токену канала (приложение-получатель)»

Откройте настройки канала → вкладка Доставка → блок WebSocket. Там доступны готовые примеры на 9 языках. Принцип одинаковый: подключаемся к wss://api.notifly.ru/ws?token=<app-токен> и читаем JSON-фреймы.

Окно терминала
npm install -g wscat
wscat -c "wss://api.notifly.ru/ws?token=A..."
Окно терминала
$ws = [System.Net.WebSockets.ClientWebSocket]::new()
$uri = [Uri]"wss://api.notifly.ru/ws?token=A..."
$ws.ConnectAsync($uri, [Threading.CancellationToken]::None).Wait()
$buf = [byte[]]::new(8192)
$seg = [ArraySegment[byte]]::new($buf)
while ($ws.State -eq 'Open') {
$res = $ws.ReceiveAsync($seg, [Threading.CancellationToken]::None).Result
$msg = [Text.Encoding]::UTF8.GetString($buf, 0, $res.Count)
Write-Host $msg
}
# pip install websockets
import asyncio, json, websockets
URL = "wss://api.notifly.ru/ws?token=A..."
async def main():
async for ws in websockets.connect(URL, ping_interval=30):
try:
async for raw in ws:
data = json.loads(raw)
if "id" in data and "message" in data:
print(data["title"], "", data["message"])
except websockets.ConnectionClosed:
continue
asyncio.run(main())
c, _, _ := websocket.DefaultDialer.Dial("wss://api.notifly.ru/ws?token=A...", nil)
defer c.Close()
for {
_, data, err := c.ReadMessage()
if err != nil { return }
fmt.Println(string(data))
}

Сообщения могут прийти, пока сокет был оборван. Чтобы ничего не потерять:

  1. Локально храните last_id — максимальный увиденный id.

  2. После переподключения дозабирайте пропущенное через REST:

    Окно терминала
    curl -s -H "X-Notifly-Key: A..." \
    "https://api.notifly.ru/message?since=<last_id>"
  3. Если хочется работать в режиме «сигнал + GET» (минимальный трафик в сокете), всё равно полное тело лежит в REST:

    Окно терминала
    curl -s -H "X-Notifly-Key: A..." \
    "https://api.notifly.ru/message/<id>"

Это превращает WebSocket в надёжный bus: «онлайн» — мгновенная доставка, «офлайн» — догон по since.

10 сценариев, где приёмником выступает приложение

Заголовок раздела «10 сценариев, где приёмником выступает приложение»

Канал Notifly — это не только «push на телефон». Это универсальная шина, на которую можно посадить любое приложение-обработчик:

  1. Auto-deploy. Канал ci-prod — отправляем сообщение из CI, CD-агент слушает WebSocket и автоматически запускает деплой указанной версии.
  2. Restart on alert. Канал restart-nginx — мониторинг шлёт алёрт, подписанный на канал sidecar выполняет systemctl restart nginx.
  3. Self-healing infra. Канал disk-full — алёрт от Prometheus, слушатель чистит логи и временные файлы.
  4. LLM-агент. Локальный LLM подписан на канал assistant-inbox, реагирует на сообщения как на задачи и пишет в обратный канал ответ.
  5. Smart home bridge. Канал home — приёмник на Raspberry Pi парсит сообщения и шлёт команды Home Assistant / MQTT.
  6. Webhook-replacement. Партнёр шлёт событие в ваш канал — сервис- обработчик слушает по WebSocket. Не нужно открывать публичный HTTP.
  7. Очередь задач для воркеров. Воркеры подписаны на канал jobs, получают задание и забирают тело через GET /message/:id.
  8. Чат-бот fan-out. Telegram/WhatsApp-бот подписан на канал outbound — пересылает каждое сообщение в нужный чат пользователя.
  9. IoT-команды. Устройство (ESP32 + MicroPython uwebsockets) слушает канал device-42 и исполняет команды (открыть реле, мигнуть LED).
  10. Cross-team relay. Канал oncall — алерт прилетает в Slack/Teams через слушатель-мост, а команда продолжает работать в своём стеке.

Во всех сценариях канал тот же самый, что используется для отправки, — просто подписываемся на него любым A...-токеном и получаем JSON в реальном времени.

Готовый python-скрипт лежит в репозитории:

Окно терминала
python3 ws-handler/test_ws.py

Он подключается, отправляет ping, ждёт pong и проверяет, что сервер зарегистрировал соединение в S3.