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-стенда.
Из браузера (JavaScript)
Заголовок раздела «Из браузера (JavaScript)»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');Из командной строки (wscat)
Заголовок раздела «Из командной строки (wscat)»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 | Описание |
|---|---|
ping | Keep-alive / проверка связи |
status | Информация о текущем соединении |
list | Список всех соединений пользователя |
broadcast | Рассылка остальным соединениям пользователя |
| любое | Эхо обратно (для отладки) |
Что шлёт сервер
Заголовок раздела «Что шлёт сервер»Push-уведомление
Заголовок раздела «Push-уведомление»Это «полезная нагрузка» — то, ради чего вообще держится сокет.
Формат полностью совпадает с MessageExternal REST API:
{ "id": 1714476672123456789, "appid": 12345, "title": "Деплой завершён", "message": "Сборка #874 ушла на прод.", "priority": 5, "extras": null, "date": "2026-04-30T10:11:12Z"}pong — ответ на ping
Заголовок раздела «pong — ответ на ping»{"action":"pong","timestamp":"2026-04-30T10:11:12Z","connection_id":"c057..."}status — состояние соединения
Заголовок раздела «status — состояние соединения»{ "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"}connections — список соединений (на запрос list)
Заголовок раздела «connections — список соединений (на запрос list)»{ "action":"connections", "count":2, "connections":[ {"connection_id":"c057...","connected_at":"...","status":"connected", ...} ]}echo — отладочное эхо неизвестных action
Заголовок раздела «echo — отладочное эхо неизвестных action»{"action":"echo","message":{"action":"foo","data":42}}Жизненный цикл соединения
Заголовок раздела «Жизненный цикл соединения»- Клиент открывает
wss://.../ws?token=.... - API Gateway →
CONNECT→ Cloud Function записываетConnectionInfo(включаяuser_id) вconnections/<id>.jsonв S3. - Клиент шлёт фреймы →
MESSAGE→ функция отвечает вResponse.Body. - REST-функция
notifly-api, создав новое сообщение, перечисляет соединения пользователя в S3 и шлёт фрейм через Management-API. - Клиент закрывает сокет →
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-фреймы.
bash (через wscat)
Заголовок раздела «bash (через wscat)»npm install -g wscatwscat -c "wss://api.notifly.ru/ws?token=A..."PowerShell (Windows 7+)
Заголовок раздела «PowerShell (Windows 7+)»$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 websocketsimport 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))}Гарантия доставки (catch-up по since)
Заголовок раздела «Гарантия доставки (catch-up по since)»Сообщения могут прийти, пока сокет был оборван. Чтобы ничего не потерять:
-
Локально храните
last_id— максимальный увиденныйid. -
После переподключения дозабирайте пропущенное через REST:
Окно терминала curl -s -H "X-Notifly-Key: A..." \"https://api.notifly.ru/message?since=<last_id>" -
Если хочется работать в режиме «сигнал + GET» (минимальный трафик в сокете), всё равно полное тело лежит в REST:
Окно терминала curl -s -H "X-Notifly-Key: A..." \"https://api.notifly.ru/message/<id>"
Это превращает WebSocket в надёжный bus: «онлайн» — мгновенная доставка,
«офлайн» — догон по since.
10 сценариев, где приёмником выступает приложение
Заголовок раздела «10 сценариев, где приёмником выступает приложение»Канал Notifly — это не только «push на телефон». Это универсальная шина, на которую можно посадить любое приложение-обработчик:
- Auto-deploy. Канал
ci-prod— отправляем сообщение из CI, CD-агент слушает WebSocket и автоматически запускает деплой указанной версии. - Restart on alert. Канал
restart-nginx— мониторинг шлёт алёрт, подписанный на канал sidecar выполняетsystemctl restart nginx. - Self-healing infra. Канал
disk-full— алёрт от Prometheus, слушатель чистит логи и временные файлы. - LLM-агент. Локальный LLM подписан на канал
assistant-inbox, реагирует на сообщения как на задачи и пишет в обратный канал ответ. - Smart home bridge. Канал
home— приёмник на Raspberry Pi парсит сообщения и шлёт команды Home Assistant / MQTT. - Webhook-replacement. Партнёр шлёт событие в ваш канал — сервис- обработчик слушает по WebSocket. Не нужно открывать публичный HTTP.
- Очередь задач для воркеров. Воркеры подписаны на канал
jobs, получают задание и забирают тело черезGET /message/:id. - Чат-бот fan-out. Telegram/WhatsApp-бот подписан на канал
outbound— пересылает каждое сообщение в нужный чат пользователя. - IoT-команды. Устройство (ESP32 + MicroPython
uwebsockets) слушает каналdevice-42и исполняет команды (открыть реле, мигнуть LED). - Cross-team relay. Канал
oncall— алерт прилетает в Slack/Teams через слушатель-мост, а команда продолжает работать в своём стеке.
Во всех сценариях канал тот же самый, что используется для отправки, —
просто подписываемся на него любым A...-токеном и получаем JSON в
реальном времени.
Ручное тестирование
Заголовок раздела «Ручное тестирование»Готовый python-скрипт лежит в репозитории:
python3 ws-handler/test_ws.pyОн подключается, отправляет ping, ждёт pong и проверяет, что сервер
зарегистрировал соединение в S3.