qvib.pro
EN

~15 мин чтения · всем · Обновлено: 28.07.2026 · Read in English

Как создать сервис видеозвонков и чата + промпт

Как создать свой сервис видеозвонков и чата: архитектура, стек и готовый промпт

Коротко

Свой сервис видеозвонков и чата — аналог Slack по текстовой части и Telemost/Google Meet по звонкам — собирается из двух независимых транспортов: текст и сигналинг идут через Socket.IO и REST в PostgreSQL, а медиа — напрямую между браузерами и медиасервером LiveKit по WebRTC. Ключевое решение — SFU вместо P2P-mesh: mesh честно работает до 3–4 участников, дальше нужен селективный форвардинг, а self-hosted LiveKit закрывает его одним контейнером. В статье — карта из 10 сервисов чата (треды, реакции, присутствие, полнотекстовый поиск с русской морфологией), три сценария входа в звонок, режимы качества с адаптивной деградацией, комнаты по ссылке с гостевым доступом, схема из 14 таблиц чата и звонков, слой безопасности и инфраструктура из 5 контейнеров. В конце — разбор готового промпта из 11 секций, которым весь сервис ставится одной задачей агенту: промпт лежит в арсенале целиком, с кнопкой копирования, и написан по работающей реализации.

Из чего состоит сервис видеозвонков и чата?

Вся система держится на одном архитектурном решении: два независимых транспорта. Текст, состояние и сигналинг идут привычным путём — Socket.IO и REST к Node-бэкенду, хранение в PostgreSQL. А медиа (аудио, видео, демонстрация экрана) через бэкенд не проходит вообще: браузеры соединяются напрямую с медиасервером LiveKit по WebRTC, а бэкенд лишь выдаёт подписанный токен доступа и управляет комнатой через серверный SDK.

Это решение определяет всё остальное. Бэкенд не упирается в трафик и масштабируется по числу сообщений, а не по числу мегабит; медиасервер масштабируется отдельно — и умирает отдельно, не роняя чат. Отсюда же главное правило безопасности: ключи LiveKit живут только на сервере, клиент их никогда не видит. Вместо ключей браузер получает JWT с конкретными правами на конкретную комнату и ограниченным сроком жизни.

Роли распределены так:

  • SPA-клиент — React + Vite: один сокет на вкладку, React Query для REST-кэша, @livekit/components-react для медиа-слоя. Виджеты чата и активного звонка живут в корне приложения и переживают навигацию.
  • API-бэкенд — Express + Socket.IO на одном HTTP-сервере: REST для истории, файлов и CRUD, сокет для всего, что должно прилетать мгновенно.
  • SFU-медиасервер — LiveKit: селективный форвардинг, simulcast, dynacast, TCP-fallback.
  • Запись — отдельный контейнер LiveKit Egress: подключается к комнате как невидимый участник, композитит сетку в MP4 и пишет на том.

Почему не «просто WebRTC»?

Наивная схема «браузеры соединяются друг с другом напрямую» называется P2P-mesh — и она честно работает до 3–4 человек. Каждый участник шлёт свой поток каждому, и уже на восьмерых собеседниках ноутбук захлёбывается кодированием.

Ответ индустрии — SFU (Selective Forwarding Unit): каждый участник шлёт один поток вверх, а сервер селективно раздаёт нужные потоки вниз. Вместе с simulcast и dynacast нагрузка на клиента перестаёт расти с числом участников.

Писать свой SFU — это месяцы работы. Self-hosted LiveKit закрывает вопрос одним контейнером и оставляет полный контроль над данными: медиа не покидает ваш сервер, в отличие от облачных API видеосвязи.

Mesh (все со всеми)
2 участника
2 потоков
4 участника
12 потоков
8 участников
56 потоков — ноутбук сдаётся
N×(N−1) соединений: честно работает до 3–4 человек
SFU (LiveKit)
2 участника
2 потоков
4 участника
4 потоков
8 участников
8 потоков
каждый шлёт один поток вверх, вниз — только нужные (simulcast, dynacast)
Почему после 3–4 участников нужен SFU: в mesh число потоков растёт квадратично, в SFU — линейно. Свой SFU — месяцы работы, self-hosted LiveKit — один контейнер.

Какой стек выбрать?

Стек ниже — не список модных технологий, а комплект с работающей реализации: каждый слой решает свою задачу и не лезет в чужую.

Слой Технология Зачем
Фронтенд React 18 + Vite SPA с виджетами чата и звонка, переживающими навигацию
Стили TailwindCSS Дизайн-токены, светлая и тёмная темы
API и реалтайм Express + Socket.IO REST для истории и CRUD, сокет для мгновенных событий
Хранилище PostgreSQL 16 Данные чата, комнат, звонков плюс полнотекстовый поиск на tsvector
Кэш и шина Redis Presence, rate-limit и адаптер Socket.IO для нескольких инстансов
Медиа LiveKit SFU WebRTC-звонки: форвардинг, simulcast, TCP-fallback
Запись LiveKit Egress Композитная запись комнаты в MP4 на том
Браузер (SPA)
React 18 + Vite · один сокет на вкладку
текст · состояние · сигналинг
Бэкенд
Express + Socket.IO: REST для истории, сокет для мгновенного · выдаёт JWT на комнату
SQL
PostgreSQL 16 + Redis
сообщения, каналы, presence · Redis-адаптер сокета
Браузер (SPA)
React 18 + Vite · один сокет на вкладку
медиа по WebRTC
LiveKit SFU
аудио, видео, экран — напрямую по WebRTC, мимо бэкенда
Два независимых транспорта: текст и сигналинг идут через бэкенд, медиапоток — напрямую в SFU. Ключи LiveKit живут только на сервере: клиент получает короткоживущий JWT с правами на конкретную комнату.

Собирать такой проект руками — долго; агентным кодингом он ставится одной детальной задачей. Если подход вам в новинку — начните с разбора, что такое агентный кодинг.

Как устроен чат внутри?

Бэкенд разложен по сервисам, каждый со своей зоной ответственности; роуты и обработчики сокета — тонкие, вся логика внутри сервисов. Десять модулей закрывают текстовую часть целиком:

Модуль Отвечает за
channelService Каналы: публичные и приватные, участники и роли, архивация, тема, настройки уведомлений на участника
messageService Отправка, редактирование, мягкое удаление, треды, закрепление, счётчик ответов, полнотекстовый поиск
reactionService Эмодзи-реакции: одна реакция от пользователя на сообщение, агрегация по эмодзи
directMessageService Личные переписки: канал типа direct создаётся на пару участников идемпотентно
chatAttachmentService Вложения до 50 МБ, превью изображений, отдача файла с проверкой доступа
bookmarkService Сохранённые сообщения с личной заметкой
presenceService Статусы online / away / dnd / offline, кастомный статус с эмодзи, heartbeat и авто-away
chatNotificationService Уведомления об упоминаниях, личных сообщениях и ответах в треде
guestService Приглашения по ссылке с набором прав, гостевые сессии, лимиты
chatAuditService Журнал значимых действий: удаления, кики, изменения прав

Пользователь при этом видит привычный Slack-подобный интерфейс: список каналов с непрочитанными и присутствием, ленту с «печатает…», тред панелью справа (у родительского сообщения виден счётчик ответов), панели «Файлы / Ссылки / Закреплённые / Участники» и упоминания через @, создающие уведомление.

Отдельно стоит отметить поиск: он построен на колонке tsvector PostgreSQL с русской морфологией и триггером обновления при вставке и правке текста. Сообщение находится по слову в любой словоформе — и отдельный поисковый движок для чата рабочей команды не нужен.

Как работают звонки?

Звонок — это всегда запись в таблице комнат звонков плюс живая комната в LiveKit; различаются только точки входа. Сценариев три, плюс один служебный:

  1. Звонок в канале — модель Slack-хаддла: участник жмёт «Позвонить», в канал падает плашка «Идёт звонок, присоединиться». Никто никого не будит, но все видят активность.
  2. Личный звонок 1:1 — классический вызов: модалка входящего с рингтоном, исходящий с отменой, таймаут, отказ и сценарий «уже в другом звонке» с предложением переключиться.
  3. Комната по ссылке — сценарий Telemost: создал комнату, скинул ссылку, собеседник зашёл — с аккаунтом или гостем по имени.
  4. Возврат в звонок — свёрнутый звонок живёт в плавающем виджете поверх приложения: можно уйти в другой раздел, продолжая говорить.

У комнаты есть режим качества, выбираемый при старте — и он меняет права в выдаваемом токене, а не галочки в интерфейсе:

Режим Что публикуется Когда выбирать
economy Микрофон и экран; камера запрещена на уровне токена Слабый канал, мобильный интернет, большие планёрки
standard Видео до 6 участников, дальше — аватары с индикацией речи По умолчанию: рабочие встречи команды
conference Видео всех участников без ограничений Демо, вебинары, презентации при хорошем канале

Поверх режима работает адаптивная деградация: фоновая задача опрашивает нагрузку медиасервера и рассылает клиентам уровень — none, moderate (снизить битрейт и частоту кадров), high (выключить камеры, оставить экран и звук). Пользователь видит честный баннер, а не самопроизвольно рассыпающуюся картинку.

Модерация внутри звонка — серверная: ведущий может замьютить участника, выключить ему видео, удалить из звонка или завершить звонок для всех, и каждое действие идёт адресной командой через сервер, а не через локальное состояние клиента. Запись включается одной кнопкой: бэкенд запускает композитную запись через Egress (сетка, HD 30 fps; для аудиозвонка — только звук), по остановке кладёт файл на том и сохраняет ссылку. Индикатор «идёт запись» видят все — это не опция, а требование и к приличию, и к закону.

Как сделать комнаты по ссылке и гостевой доступ?

Комната — самостоятельная сущность, не привязанная к каналу: имя, описание, уникальный токен ссылки и две оси настроек. Тип: permanent (переиспользуется бесконечно) или one-time (удаляется после завершения звонка). Доступ: org-only (только свои по логину) или public (любой, у кого есть ссылка, включая гостей без аккаунта).

У публичной комнаты своя страница по токену: превью, поле «Как вас зовут», выбор камеры и микрофона до входа. Внутри — тот же звонок плюс текстовый чат комнаты, где пишут и пользователи, и гости.

Гостевая ссылка — это не «дырка в периметре», а токен с явным набором прав: canChat, canCall, canViewHistory, лимит числа использований и срок годности. Гость получает свою сессию, урезанные лимиты действий и не видит ничего за пределами выданного канала или комнаты. Отозвать ссылку можно в один клик.

Как выглядит схема данных?

Всю функциональность закрывают 14 таблиц чата и звонков (плюс базовые users и organizations), и у каждой сущности организации есть organization_id — база изоляции арендаторов: любой запрос фильтруется по нему прямо в SQL. Ключевые таблицы:

  • chat_channels и chat_channel_members — каналы (public/private/direct), членство, роли и отметки прочитанности;
  • chat_messages — сообщения и треды: parent_message_id, упоминания, edited_at/deleted_at, закрепление, search_vector;
  • chat_message_reactions, chat_attachments, chat_bookmarks — реакции, файлы с превью, сохранённые;
  • chat_user_presence и chat_notifications — присутствие и уведомления;
  • chat_rooms и chat_room_messages — комнаты по ссылке и их чат (автор — пользователь или гостевая сессия);
  • chat_call_rooms и chat_call_participants — сессии звонков (имя комнаты LiveKit, режим, ссылка на запись) и история участия;
  • chat_guest_invites и chat_guest_sessions — приглашения с правами и активные гости.

Полный DDL с полями, индексами и триггером поиска целиком лежит в промпте, о котором ниже.

Что с безопасностью?

Слой безопасности — часть архитектуры, а не приложение к ней; принцип один: запрещено, пока явно не разрешено.

  • Изоляция арендаторов. Каждый запрос к базе фильтруется по организации пользователя. Не «фронт не покажет», а условие в SQL.
  • Токены LiveKit выдаются сервером на конкретную комнату, личность и права публикации, с ограниченным сроком. В режиме economy право публиковать камеру просто не выдаётся — запрет невозможно обойти из консоли браузера.
  • Проверка прав на входе в звонок: членство в канале, доступ к комнате, валидный гостевой токен.
  • Валидация пейлоадов сокета схемами: сокет — такая же граница доверия, как HTTP.
  • Лимиты частоты и на HTTP, и на события сокета — свои для пользователя, гостя и анонима.
  • Файлы отдаются только через проверку доступа; тип и размер проверяются на сервере.
  • Запись видна всем участникам и пишется в журнал аудита.

Какая инфраструктура нужна?

Весь сервис — это пять сервисных контейнеров — приложение, PostgreSQL, Redis, LiveKit и LiveKit Egress — плюс nginx на входе, шестой сервис compose. Порты медиасервера: 7880 — сигналинг по WebSocket, 7881 — TCP-фолбэк для строгих корпоративных сетей, и диапазон UDP 50000–52000 для медиа: примерно две тысячи портов дают запас на сотни одновременных участников.

nginx + TLS
прокси WebSocket (Upgrade/Connection) · client_max_body_size под файлы · без HTTPS браузер не даст камеру
app
Express + Socket.IO + SPA
postgres 16
14 таблиц чата и звонков
redis
presence · rate-limit · адаптер Socket.IO
livekit
7880 WS-сигналинг · 7881 TCP-фолбэк · UDP 50000–52000 медиа
egress
запись: композит сетки в MP4 на том
Пять сервисных контейнеров + nginx на входе: медиасервер масштабируется и умирает отдельно от бэкенда. Диапазон UDP ~2000 портов даёт запас на сотни одновременных участников.

Перед приложением стоит nginx с тремя обязанностями, о которых чаще всего забывают: проксирование WebSocket (заголовки Upgrade и Connection), поднятый client_max_body_size под вложения — и терминация TLS. Последнее не опция: WebRTC требует защищённого контекста, без HTTPS камера и микрофон просто не включатся.

На каких граблях теряют недели?

  • ICE-кандидаты с адресом Docker-моста. Медиасервер в контейнере рекламирует внутренний адрес — соединение не устанавливается ни у кого извне. Лечится явным указанием публичного адреса узла и исключением мостов из списка интерфейсов.
  • Забытый лимит тела в nginx. Загрузка файла больше мегабайта возвращает 413 и никогда не доходит до бэкенда, где лимит выставлен честно.
  • Сокет на компонент вместо приложения. Пересоздание соединения при каждом ререндере даёт лавину подключений и потерю событий.
  • Балансировщик без Redis-адаптера (или без липких сессий). События уходят в инстанс, где нет получателя.
  • Гость без ограничений. Ссылка без срока, без лимита использований и без набора прав — это не приглашение, а бэкдор.
  • Нет сценария «уже в звонке». Пользователь оказывается в двух комнатах сразу, а на сервере остаются висящие участники.
  • Права публикации только на клиенте. Всё, что не запрещено в токене, будет включено — рано или поздно.

Готовый промпт: сервис одной задачей агенту

Всё описанное выше собирается агентом (Claude Code, Cursor, Codex — любым, кто умеет писать файлы и запускать команды) из одного детального промпта. В статью мы его не вставляем — в нём 18 КБ текста, он живёт в карточке арсенала: промпт «Сервис видеозвонков под ключ» — целиком, с кнопкой копирования.

Устройство промпта важнее объёма. В нём 11 секций: точный стек с версиями → схема базы с индексами и триггером поиска → карта REST-эндпоинтов → события сокета в обе стороны → интеграция LiveKit с логикой токенов → экраны и инвентарь кнопок → дизайн-токены → инфраструктура (docker-compose, livekit.yaml, nginx) → безопасность → порядок работы по этапам → критерии приёмки — 12 проверяемых пунктов, от «два браузера переписываются в реальном времени» до «чужая организация не получает ни одного объекта ни по API, ни по сокету».

Именно критерии приёмки превращают промпт из пожелания в задачу: агент работает этапами, после каждого запускает сборку и тесты и проверяет себя по списку. На этом принципе стоит весь вайб-кодинг: человек описывает результат и способ проверки, агент доводит до готовности. Начать удобнее всего с гайда по Claude Code.

11
секций ТЗ в промпте: от стека до критериев приёмки
14
таблиц чата и звонков — полная схема с индексами
8
этапов сборки: после каждого — тесты и линтер
12
проверяемых критериев приёмки в финале
Промпт под ключ в цифрах: агент получает не «сделай чат», а полное ТЗ.

Частые вопросы

Можно ли обойтись без LiveKit?

Для звонков 1:1 и на 3–4 человека хватит и чистого WebRTC в режиме mesh. Но дальше без SFU не обойтись, а писать свой — месяцы работы. Альтернатива LiveKit — облачные API видеосвязи, но тогда медиа уходит на чужие серверы; self-hosted LiveKit оставляет данные у вас и ставится одним контейнером.

Сколько нужно контейнеров и потянет ли один сервер?

Шесть в одном docker-compose: приложение, PostgreSQL, Redis, LiveKit, Egress и nginx на входе. Всё поднимается на одном сервере. Диапазон UDP 50000–52000 даёт запас на сотни одновременных участников, а Redis-адаптер Socket.IO позволяет позже разнести приложение на несколько инстансов за балансировщиком.

Почему нельзя отдать ключи LiveKit фронтенду?

Ключи подписывают токены доступа — владея ими, любой посетитель выпишет себе токен в любую комнату с любыми правами. Поэтому ключи живут только на сервере, а клиент получает JWT на конкретную комнату с конкретными правами публикации и сроком жизни. Даже запрет камеры в режиме economy зашит в токен — его не обойти из консоли браузера.

Законно ли записывать звонки?

Запись должна быть явной для всех участников: в этой архитектуре индикатор «идёт запись» видят все, он не скрывается, а старт записи пишется в журнал аудита. Молчаливая запись недопустима ни этически, ни юридически.

Подойдёт ли такая архитектура для тысячи участников?

Описанная конфигурация рассчитана на командный сервис: сотни одновременных участников на одном сервере. SFU-архитектура масштабируется и дальше — медиасервер растёт отдельно от бэкенда, — но вебинары на тысячи зрителей потребуют других решений поверх той же основы. Что ещё реально собрать по такой же схеме — в подборке что можно создать вайб-кодингом.

Читайте также