qvib.pro
EN

Сервис чата и звонков: промт под ключ

Для чего: один промт — и ИИ-агент собирает self-hosted аналог Slack + Meet: каналы, треды, звонки через LiveKit, запись, гости по ссылке. Docker, 8 этапов.

бесплатно open-source профи

LiveKit — открытый SFU: медиа-слой сервиса, self-hosted проверено 2026-07-28

Обновлено: 28.07.2026

$ Ты — старший full-stack инженер. Собери с нуля production-ready сервис команд…
Сервис чата и звонков: промт под ключ

Когда использовать

Когда команде нужен свой сервис общения, а не аренда чужого: корпоративный мессенджер с хранением данных у себя, телемедицина с видеоконсультациями, онлайн-обучение с комнатами для занятий, закрытое комьюнити со звонками по ссылке. На выходе — self-hosted аналог Slack по текстовой части и Google Meet по звонкам: монорепозиторий с бэкендом на Node.js + TypeScript, фронтом на React и docker-compose, где рядом с приложением поднимаются PostgreSQL, Redis, медиасервер LiveKit, Egress для записи и nginx.

Это не «сделай мне чатик», а полноценное техзадание на 18,8 КБ: стек зафиксирован до версий, схема базы расписана по колонкам, перечислены все REST-маршруты, сокет-события и даже кнопки нижней панели звонка. Агенту остаётся исполнять, а не догадываться — в этом и смысл: чем меньше свободы в архитектурных развилках, тем предсказуемее результат. Как устроен такой способ работы — в статье про агентный кодинг.

Промт (бери и вставляй)

Отдайте промт целиком агентному инструменту — Claude Code, Cursor в агентном режиме или Codex — в пустую папку нового проекта. Промт самодостаточен: агент не будет задавать уточняющих вопросов, при развилках выберет описанный вариант и зафиксирует решение в README.

Ты — старший full-stack инженер. Собери с нуля production-ready сервис командного чата и видеозвонков: аналог Slack по текстовой части и аналог Telemost/Google Meet по звонкам. Работай самостоятельно до полностью работающего результата: создай структуру репозитория, код, миграции базы, docker-compose, seed-данные и README с командами запуска. Не задавай уточняющих вопросов — при развилке выбирай вариант, описанный ниже, и фиксируй решение в README.

=== 1. СТЕК (использовать именно его) ===
Backend: Node.js 20, TypeScript, Express 4, Socket.IO 4, PostgreSQL 16 (драйвер pg), Redis 7 (кэш, presence, rate-limit, адаптер @socket.io/redis-adapter), zod для валидации, multer для загрузок, sharp для превью, helmet, express-rate-limit + rate-limit-redis, pino для логов, jsonwebtoken для сессий, livekit-server-sdk 2.x.
Frontend: React 18, TypeScript, Vite 5, TailwindCSS 3, React Router 6, @tanstack/react-query 5, socket.io-client 4, livekit-client + @livekit/components-react 2, lucide-react для иконок, zustand для локального состояния звонка.
Медиа: self-hosted LiveKit (SFU) + LiveKit Egress для записи. Никакого самописного SFU, никакого mesh-WebRTC.
Тесты: vitest на фронте, jest + supertest на бэке.

Структура монорепозитория:
  backend/src/{api,sockets,services,repositories,middleware,db/migrations,utils}
  frontend/src/{pages,features/chat,components/call,hooks,api,styles}
  docker/{docker-compose.yml,livekit/livekit.yaml,livekit/egress.yaml,nginx/nginx.conf}

Правила кода: слой репозиториев отвечает за SQL, сервисы — за логику, роуты и обработчики сокета тонкие. Все ответы API в формате { success, data } либо { success, error }. Все таблицы и запросы фильтруются по organization_id. Никаких секретов в коде — только переменные окружения.

=== 2. БАЗА ДАННЫХ ===
Сделай версионированные миграции (up/down), запускаемые командой npm run migrate. Кроме таблиц users и organizations (id, name, created_at) создай:

chat_channels(id uuid pk, organization_id uuid fk, name varchar(100), slug varchar(100), description text, type varchar(20) check in (public, private, direct), topic text, created_by uuid fk, is_archived bool default false, metadata jsonb, created_at, updated_at, unique(organization_id, slug))
chat_channel_members(channel_id uuid, user_id uuid, role varchar(20) check in (owner, admin, member), joined_at, last_read_message_id uuid, last_read_at, notifications varchar(20) check in (all, mentions, none), is_muted bool, pk(channel_id, user_id))
chat_messages(id uuid pk, channel_id uuid fk cascade, user_id uuid fk, parent_message_id uuid fk null, content text, content_type varchar(20) check in (text, file, system), mentions jsonb default [], edited_at, deleted_at, is_pinned bool, thread_reply_count int default 0, thread_last_reply_at, search_vector tsvector, created_at, updated_at)
chat_message_reactions(message_id uuid, user_id uuid, emoji varchar(50), created_at, pk(message_id, user_id, emoji))
chat_attachments(id uuid pk, message_id uuid, channel_id uuid, file_name, original_name, file_path, file_size bigint, mime_type, thumbnail_path, width int, height int, uploaded_by uuid, created_at)
chat_bookmarks(user_id uuid, message_id uuid, note text, created_at, pk(user_id, message_id))
chat_user_presence(user_id uuid pk, organization_id uuid, status varchar(20) check in (online, away, dnd, offline), custom_status_text varchar(100), custom_status_emoji varchar(50), last_active_at, updated_at)
chat_notifications(id uuid pk, user_id uuid, channel_id uuid, message_id uuid, type varchar(20) check in (mention, dm, thread_reply, channel), is_read bool, created_at)
chat_rooms(id uuid pk, organization_id uuid, created_by uuid, name varchar(255), description text, room_type varchar(20) check in (permanent, one-time), access_level varchar(20) check in (org-only, public), unique_link_token varchar(64) unique, is_active bool default false, call_ended_at, created_at, updated_at, deleted_at)
chat_room_messages(id uuid pk, room_id uuid, user_id uuid null, guest_session_id uuid null, content text, created_at, check(user_id is not null or guest_session_id is not null))
chat_call_rooms(id uuid pk, organization_id uuid, channel_id uuid null, chat_room_id uuid null, livekit_room_name varchar(100) unique, title varchar(200), type varchar(20) check in (audio, video), call_mode varchar(16) default standard check in (economy, standard, conference), created_by uuid, started_at, ended_at, max_participants int default 100, recording_enabled bool default false, recording_url text, metadata jsonb)
chat_call_participants(id uuid pk, room_id uuid, user_id uuid, joined_at, left_at, participant_sid varchar(100))
chat_guest_invites(id uuid pk, organization_id uuid, channel_id uuid null, call_room_id uuid null, chat_room_id uuid null, token varchar(64) unique, guest_name varchar(100), created_by uuid, permissions jsonb default {"canChat":true,"canCall":true,"canViewHistory":false}, max_uses int default 1, used_count int default 0, expires_at, is_active bool default true, created_at, check(хотя бы одна из трёх целей не null))
chat_guest_sessions(id uuid pk, invite_id uuid, organization_id uuid, guest_name varchar(100), guest_token varchar(128) unique, channel_id uuid null, call_room_id uuid null, joined_at, last_active_at, left_at, metadata jsonb)

Индексы: (channel_id, created_at desc) на сообщениях, GIN на search_vector, GIN на mentions, частичные индексы по token у приглашений, сессий и комнат, (organization_id) where deleted_at is null у комнат, (room_id, created_at) у сообщений комнаты.
Триггер полнотекстового поиска: tsvector_update_trigger(search_vector, pg_catalog.russian, content) на insert/update of content.

=== 3. BACKEND: REST API ===
Все маршруты под /api/chat, все требуют авторизации, кроме публичных маршрутов комнат и гостевого входа.
/channels: POST / (создать), GET / (мои каналы с непрочитанными), GET /:id, PUT /:id, DELETE /:id (архивация), GET /:id/members, POST /:id/members, DELETE /:id/members/:userId, POST /:id/read, PUT /:id/notifications
/messages: GET /channel/:channelId (курсорная пагинация по created_at), POST /, PUT /:id (только автор, ставит edited_at), DELETE /:id (мягкое: deleted_at), GET /:id/thread, POST /:id/reactions, DELETE /:id/reactions/:emoji, POST /:id/pin, GET /search?q= (полнотекстовый по каналам пользователя)
/dm: POST / (открыть или создать переписку с пользователем идемпотентно), GET /
/attachments: POST /upload (multipart, лимит 50 МБ, белый список mime, превью для изображений через sharp), GET /:id, GET /:id/thumbnail, GET /channel/:channelId, DELETE /:id
/calls: POST /start (создаёт комнату LiveKit и запись звонка, возвращает токен и URL), POST /:roomId/join, POST /:roomId/leave, POST /:roomId/end, POST /force-leave, POST /:roomId/kick/:userId, POST /:roomId/mute/:userId, POST /:roomId/disable-video/:userId, PATCH /:roomId/mode, GET /active, GET /history, GET /:roomId/participants, POST /:roomId/recording/start, POST /:roomId/recording/stop, GET /:roomId/recording/status, GET /recordings, GET /recordings/:filename, POST /anonymous-token
/rooms: POST /, GET /, GET /:id, PATCH /:id, DELETE /:id, POST /:id/join, GET /:id/messages, POST /:id/messages, GET /:id/active-call, POST /:id/end-call, GET /public/:token, POST /public/:token/join, POST /public/:token/messages
/guests: POST /invites, GET /invites, DELETE /invites/:id, POST /join/:token, GET /session, GET /sessions
прочее: POST /bookmarks, DELETE /bookmarks/:messageId, GET /bookmarks, GET /notifications, POST /notifications/read, GET /presence, POST /presence/status

=== 4. BACKEND: SOCKET.IO ===
Аутентификация в middleware соединения: JWT пользователя, либо гостевой токен, либо анонимный токен публичной комнаты. Три роли: user, guest, anonymous.
Комнаты сокета: chat:{channelId}, call:{roomId}, chatroom:{roomId}, user:{userId}, presence:{orgId}.
От клиента: chat:joinChannel, chat:leaveChannel, chat:sendMessage, chat:editMessage, chat:deleteMessage, chat:addReaction, chat:removeReaction, chat:togglePin, chat:typingStart, chat:typingStop, chat:markRead, chat:openDM, call:start, call:join, call:leave, call:end, call:kick_participant, call:mute_participant, call:disable_video, personal_call:initiate, personal_call:accepted, personal_call:rejected, personal_call:cancelled, presence:online, presence:away, presence:dnd, presence:setCustomStatus, presence:heartbeat, chatroom:subscribe, guest:auth, guest:sendMessage, guest:joinCall, guest:leaveCall
От сервера: chat:newMessage, chat:messageEdited, chat:messageDeleted, chat:reactionAdded, chat:reactionRemoved, chat:typing, chat:read, chat:channelUpdated, call:started, call:joined, call:incoming, call:active, call:participantJoined, call:participantLeft, call:participantKicked, call:kicked, call:participant_muted, call:participant_video_changed, call:forceEnd, call:ended, call:already_in_call, call:degradation_changed, call:error, personal_call:incoming, personal_call:timeout, room:statusChanged, room:participantJoined, room:participantLeft, room:callEnded, room:newMessage, presence:changed
Валидируй payload каждого события схемой zod. Rate-limit на событие и роль через Redis: chat:sendMessage 10/10с (гость 5), chat:typing* 30/10с, реакции 20/10с, chat:markRead 60/10с, call:join и call:leave 30/60с, аноним по умолчанию 0 (запрещено), кроме событий звонка в публичной комнате.

=== 5. ИНТЕГРАЦИЯ LIVEKIT ===
Сервис livekitService на бэкенде:
- generateToken(roomName, identity, name, canPublish, canSubscribe, callMode): AccessToken с grant { roomJoin: true, room, canPublish, canSubscribe, canPublishData: true }; для режима economy добавь canPublishSources: [microphone, screen_share, screen_share_audio], чтобы камера была запрещена на уровне токена, а не UI. TTL токена 6 часов.
- createRoom / deleteRoom / listParticipants / removeParticipant / muteTrack через RoomServiceClient.
- Имя комнаты генерируй как call-{первые 8 символов org}-{первые 8 символов channel или room}-{метка времени в base36}.
- Загружай livekit-server-sdk через try/catch и держи флаг доступности: без SDK приложение должно стартовать, а звонки — отдавать понятную ошибку.
Сервис egressService: startRecording (RoomCompositeEgress, layout grid, preset HD_30, audioOnly для аудиозвонка, вывод в MP4 на смонтированный том), stopRecording, статус, список файлов, отдача файла с проверкой доступа. Идентификаторы активных записей держи в Redis, а не в памяти процесса.
Фоновая задача раз в 30 секунд: опрашивай нагрузку медиасервера и, при превышении порогов, рассылай в комнаты звонков call:degradation_changed с уровнем none / moderate / high. Клиент на moderate снижает битрейт и частоту кадров, на high выключает камеры и показывает баннер.
Клиент подключается через LiveKitRoom из @livekit/components-react с adaptiveStream и dynacast; в режиме standard видео рендерится максимум для 6 участников, остальные — аватарами с подсветкой говорящего.

=== 6. FRONTEND: ЭКРАНЫ ===
Маршруты: /chat (основное окно), /chat/:channelId, /rooms (список комнат), /rooms/:id (комната со звонком и чатом), /call/:roomId (полноэкранный звонок), /join/:token (публичный вход в комнату), /guest/:token (гостевой вход в канал).
1) Окно чата: три колонки — боковая панель (поиск, кнопка «Новый чат», список каналов с непрочитанными, список личных чатов со статусом присутствия, блок «Комнаты» с бейджем идущего звонка), лента сообщений (группировка по автору, разделители дат, «печатает…», подгрузка истории вверх, при наведении на сообщение — реакция, ответить в треде, закрепить, сохранить, редактировать, удалить), правая панель по требованию (тред, участники, файлы, ссылки, закреплённые, поиск).
2) Поле ввода: автоувеличение высоты, отправка по Enter и перенос по Shift+Enter, кнопка вложения с drag-and-drop и вставкой из буфера, выбор эмодзи, упоминания по @ с выпадающим списком, предпросмотр прикреплённых файлов с удалением.
3) Экран перед звонком: превью с камеры, выпадающие списки камеры, микрофона и динамика, тумблеры камеры и микрофона, индикатор уровня звука, поле имени для гостя, кнопка «Присоединиться».
4) Экран звонка: тёмная сцена; раскладки «сетка», «докладчик», «бок о бок»; карусель участников при большом количестве; перетаскиваемое своё видео; шапка с названием, таймером, кнопками свернуть, полный экран, закрыть; нижняя панель управления (см. ниже); панели чата и участников выезжают справа; тосты о входе и выходе участников; баннер деградации; индикатор записи.
5) Комнаты: сетка карточек (название, тип, доступ, бейдж «Идёт звонок», число участников) и кнопки «Войти», «Скопировать ссылку», «Настроить», «Удалить»; модалка создания комнаты с выбором постоянная/одноразовая и только для команды/по ссылке.
6) Глобальные элементы: модалка входящего личного звонка с рингтоном (принять / отклонить), модалка исходящего с отменой, плавающий виджет свёрнутого звонка с таймером, микрофоном, завершением и разворачиванием, тосты о новых сообщениях.

Инвентарь кнопок нижней панели звонка (реализовать все, с подписями для скринридера и подсказками при наведении):
микрофон вкл/выкл · камера вкл/выкл · демонстрация экрана · чат (с бейджем непрочитанных) · участники (со счётчиком) · запись старт/стоп (с состоянием «недоступно» и «загрузка») · настройки устройств · «Ещё» (мобильное меню: запись, смена камеры, настройки) · «Завершить» — в звонке канала раскрывается меню «Выйти» / «Завершить для всех».
В списке участников у ведущего: замьютить, выключить видео, удалить из звонка. Все действия ведущего идут через сервер, а не через локальное состояние.

=== 7. ДИЗАЙН ===
Не используй стандартные шаблоны админок. Дизайн-токены задай в tailwind.config и CSS-переменных, поддержи светлую и тёмную темы приложения.
Палитра: фон приложения — холодный нейтральный (светлая тема #EEF0F3, тёмная #0C0F14), поверхности на тон светлее фона, текст #12161C / #E7ECF3, приглушённый #6A7585 / #808C9C, границы едва заметные. Акцент — один: сигнальный красно-оранжевый (#CE3A20 в светлой, #FF5A3C в тёмной) для активного звонка, записи и деструктивных действий. Семантика отдельно от акцента: онлайн зелёный, отошёл жёлтый, не беспокоить красный.
Сцена звонка всегда тёмная, независимо от темы приложения: фон #0B0E12, панель управления — полупрозрачное стекло (blur, белая заливка 10% прозрачности), круглые кнопки 48px на мобильном и скруглённые квадраты 44px на десктопе.
Типографика: моноширинный шрифт для меток, счётчиков, таймеров и технических значений; системный гротеск для текста интерфейса и сообщений; размеры 12 / 13 / 15 / 17 / 20 / 24; цифры в колонках — табличные.
Состояния: у каждой кнопки — обычное, наведение, нажатие, активное, отключённое и видимый фокус с клавиатуры. Пустые состояния (нет сообщений, нет комнат, никого в звонке) нарисуй осмысленно, с подсказкой к первому действию, а не пустым экраном.
Адаптивность: на десктопе три колонки, на планшете две с выезжающими панелями, на мобильном — одна колонка и нижняя панель; сенсорные цели не меньше 44px; уважай prefers-reduced-motion.

=== 8. ИНФРАСТРУКТУРА ===
docker-compose с сервисами: app (backend + собранный фронт), postgres 16, redis 7, livekit (порты 7880, 7881, диапазон UDP 50000-52000), egress (общий Redis, том recordings), nginx.
livekit.yaml: задай rtc.node_ip публичным адресом сервера, use_external_ip: true, tcp_port: 7881, port_range 50000-52000, в списке интерфейсов оставь только внешний и исключи docker-мосты — иначе в ICE-кандидатах окажется адрес контейнера и соединение не установится ни у кого извне.
egress.yaml: ws_url на медиасервер, redis, file_output в /recordings, preset room_composite HD_30.
nginx: проксирование WebSocket (заголовки Upgrade и Connection), client_max_body_size 200m, TLS — без HTTPS браузер не даст доступ к камере и микрофону.
Переменные окружения: DATABASE_URL, REDIS_URL, JWT_SECRET, LIVEKIT_URL, LIVEKIT_EXTERNAL_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, RECORDINGS_PATH, EGRESS_RECORDINGS_PATH, UPLOAD_PATH, MAX_FILE_SIZE.
Socket.IO подключи Redis-адаптер, чтобы приложение масштабировалось в несколько инстансов.

=== 9. БЕЗОПАСНОСТЬ (обязательно) ===
Каждый SQL-запрос фильтруется по organization_id пользователя. Доступ к каналу проверяется по членству, к комнате — по уровню доступа, к гостевой ссылке — по активности, сроку и лимиту использований; принцип «запрещено, пока явно не разрешено». Ключи LiveKit никогда не уходят на клиент. Права публикации задаются в токене. Загрузки проверяются по mime и размеру на сервере и отдаются только через проверку доступа. Все действия ведущего проверяются на сервере повторно. Значимые действия (удаление сообщений, кик, старт записи) пиши в журнал аудита. Индикатор записи обязателен и не скрывается.

=== 10. ПОРЯДОК РАБОТЫ ===
Этап 1: репозиторий, docker-compose, миграции, авторизация, seed из двух организаций и пяти пользователей.
Этап 2: каналы, сообщения, сокет, лента с историей — рабочий текстовый чат.
Этап 3: треды, реакции, закреплённые, сохранённые, вложения, поиск, упоминания, уведомления, присутствие.
Этап 4: LiveKit, токены, звонок в канале, экран звонка с полной панелью управления, раскладки, устройства.
Этап 5: личные звонки 1:1, глобальные модалки и виджет свёрнутого звонка.
Этап 6: комнаты со ссылками, публичная страница входа, гости и приглашения с правами.
Этап 7: запись через Egress, модерация, режимы качества, адаптивная деградация.
Этап 8: тесты, README, чеклист приёмки.
После каждого этапа: запусти сборку и тесты, исправь ошибки типов и линтера, и только потом переходи к следующему.

=== 11. КРИТЕРИИ ПРИЁМКИ ===
1) Два пользователя в разных браузерах переписываются в канале в реальном времени, видят «печатает», реакции и непрочитанные.
2) Ответ в треде не засоряет канал, у родительского сообщения растёт счётчик ответов.
3) Файл до 50 МБ загружается, у изображения появляется превью, файл виден в панели «Файлы».
4) Поиск находит сообщение по слову в русской словоформе.
5) Звонок в канале: второй участник видит плашку и присоединяется; работают микрофон, камера, демонстрация экрана, смена раскладки и выбор устройств.
6) Личный звонок: у собеседника всплывает входящий, работают приём, отказ и таймаут.
7) Комната по ссылке: гость без аккаунта вводит имя, видит превью, входит в звонок и пишет в чат комнаты.
8) Ведущий мьютит, выключает видео и удаляет участника; «Завершить для всех» закрывает комнату у всех.
9) Запись стартует и останавливается, файл появляется в списке и скачивается; индикатор записи видят все.
10) Свёрнутый звонок продолжается при переходе в другой раздел приложения.
11) Пользователь из другой организации не получает ни одного объекта чужой организации ни по API, ни по сокету.
12) Интерфейс работает на мобильном: одна колонка, панель снизу, сенсорные цели не меньше 44px.

В README опиши команды запуска, переменные окружения, порядок поднятия контейнеров и известные ограничения.

Как запускать

Промт ведёт агента по восьми этапам, и после каждого требует прогнать сборку и тесты, прежде чем идти дальше:

Этап Что появляется
1 Репозиторий, docker-compose, миграции, авторизация, seed-данные
2 Каналы, сообщения, сокет — рабочий текстовый чат
3 Треды, реакции, вложения, поиск, упоминания, presence
4 LiveKit, токены, звонок в канале, экран звонка
5 Личные звонки 1:1, модалки входящего, виджет свёрнутого звонка
6 Комнаты со ссылками, гостевой вход, приглашения с правами
7 Запись через Egress, модерация, режимы качества, деградация
8 Тесты, README, чеклист приёмки

Из инфраструктуры понадобятся Docker и docker-compose — весь стек поднимается контейнерами. Для проверки звонков между устройствами нужен домен с TLS: без HTTPS браузер не даст доступ к камере и микрофону, это ограничение WebRTC, а не проекта. Текстовый чат (этапы 1–3) можно гонять и на localhost.

Удобная тактика — скармливать этапы порциями: «выполни этапы 1–2, покажи результат», проверить руками, затем продолжать. Так проще поймать проблему на фундаменте, чем разбирать её под этажами звонков.

Что получится

Краткий инвентарь того, что описано в промте:

  • Чат: публичные, приватные и личные каналы; треды со счётчиком ответов; реакции; закреплённые и сохранённые сообщения; полнотекстовый поиск с русской морфологией; файлы до 50 МБ с превью изображений; статусы присутствия и «печатает…»; упоминания и уведомления.
  • Звонки — три сценария: звонок в канале (участники видят плашку и присоединяются), личный 1:1 с входящим вызовом и таймаутом, комнаты с постоянной ссылкой, куда гость заходит без аккаунта — вводит имя и попадает в звонок и чат комнаты.
  • Режимы звонка economy / standard / conference: в economy камера запрещена на уровне токена LiveKit, а не галочкой в интерфейсе. Плюс запись в MP4 через Egress, модерация ведущим (мьют, выключение видео, кик) и адаптивная деградация качества под нагрузкой.
  • База: 14 таблиц чата и звонков плюс users и organizations, версионированные миграции, индексы под ленту и поиск. Каждый запрос фильтруется по organization_id — мультитенантная изоляция заложена в схему, а не прикручена сверху.
  • Приёмка: 12 проверяемых критериев — от «два браузера переписываются в реальном времени» до «пользователь чужой организации не получает ни одного объекта ни по API, ни по сокету».

Вариации

  • Только чат, без звонков. Выкиньте секцию 5 («Интеграция LiveKit») и всё звонковое из секций 6–7 (экран звонка, панель управления, сцена), из этапов оставьте 1–3 и 8. Получится самодостаточный командный мессенджер — LiveKit и Egress из docker-compose тоже убираются.
  • Только комнаты-звонки, без мессенджера. Аналог «своего Telemost»: ядро — секции 5 и 6 (LiveKit, экраны звонка и комнат) плюс таблицы комнат и гостей. Каналы, треды и поиск можно вырезать.
  • Заменить детали стека. Другой UI-кит вместо Tailwind, другая иконка-библиотека, свой дизайн — правьте секции 1 и 7 смело. А вот двухтранспортную архитектуру не трогайте: Socket.IO для сигналинга и событий чата, LiveKit (SFU) для медиа. Попытка «упростить» до самописного WebRTC-mesh — типовой путь к проекту, который работает у двоих и разваливается у пятерых.

Про-советы

  • TLS — не «потом», а сразу. Самая частая жалоба на такие проекты — «камера не включается»: браузер блокирует getUserMedia без HTTPS. Заведите домен и сертификат до этапа 4, иначе будете отлаживать звонки, которые не могут начаться по определению.
  • ICE и docker-мост. В промте не зря расписан livekit.yaml: rtc.node_ip публичным адресом, use_external_ip и исключение docker-интерфейсов. Если этого не сделать, в ICE-кандидаты попадёт адрес контейнера — и соединение не установится ни у кого извне. Если звонок «висит на подключении», начинайте отсюда.
  • Проверяйте критерии приёмки по одному. Агент охотно рапортует «всё готово» — не верьте на слово. Пройдите все 12 пунктов руками, в двух браузерах, с реальным файлом и реальным гостем по ссылке. Особенно пункт про изоляцию организаций: это тот баг, который в проде стоит дороже всех остальных вместе.
  • Ключи LiveKit не отдаются фронту. API-ключ и секрет живут только на бэкенде, клиент получает короткоживущий токен с правами, зашитыми на сервере. Если в ревью увидите LIVEKIT_API_SECRET в коде фронта — агент срезал угол, требуйте переделать.

Read in English →