~/goydacord инженерная сторона
Как это
устроено
Голосовой мессенджер с серверами, звонками, демонстрацией экрана и клиентом для Windows. Ниже архитектура, основные решения и цифры.
- 354теста бэкендаPHPUnit, CI на каждый пуш
- 580+тестов фронтендаJest + Testing Library
- 1080p·60показ экранаH.264 на видеокарте
- 98Lighthouse, десктопдоступность и SEO: 100
01 · архитектура
Кто с кем разговаривает
Страницы и API идут через nginx, события по WebSocket, а голос, видео и экран идут мимо бэкенда, прямо в медиасервер. Бэкенд только выдаёт токены комнат и следит за правами.
02 · решения
Что сделано и зачем
Что пришлось решать по ходу и чем это закончилось.
01Звук и видео
SFU вместо сетки P2P
В звонке на пятерых по схеме P2P каждый отправляет свой поток четырём людям и упирается в исходящий канал. Поэтому голосовые каналы, личные и групповые звонки идут через свой LiveKit SFU: каждый отправляет поток один раз, дальше его раздаёт сервер.
Медиа ходит через один UDP-порт, для строгих сетей есть TCP и свой TURN (coturn).
Модерацию (заглушить, отключить звук, перетащить в другой канал) проверяют права на сервере, и применяется она прямо в комнате.
02Демонстрация экрана
1080p · 60 кадров без нагрузки на процессор
LiveKit по умолчанию кодирует видео в VP8 программно, и на 1080p60 это около двух ядер процессора. У того, кто стримит, начинает проседать игра.
Экран публикуется в H.264, и Chromium отдаёт кодирование видеокарте (NVENC, AMF, Quick Sync). Когда сеть не вывозит, жертвуем резкостью и держим частоту кадров: в игре плавность важнее.
Если видеокарта умеет кодировать H.264, процессор остаётся игре.
03Шумодав
Нейросеть прямо в браузере
DeepFilterNet скомпилирован в WebAssembly и работает в AudioWorklet у самого пользователя: голос не уходит на сервер ради обработки. Есть вариант полегче, RNNoise, он переключается в настройках.
Модель весит 17 МБ, поэтому nginx отдаёт её заранее сжатой (gzip_static), без сжатия на лету.
Клавиатура, кулер и улица вырезаются до отправки звука.
04Файлы
Гигабайты с докачкой
Файл уходит кусками по 16 МБ. Если связь оборвалась, загрузка продолжается с того же места.
Небольшие вложения шифруются на диске целиком, большие отдаются потоком с перемоткой (HTTP Range). У каждого аккаунта квота 2 ГБ; если на диске кончается место, первыми уходят давно не открытые файлы.
Видео на гигабайт отправляется так же, как картинка.
05Хранение
Переписка зашифрована в базе
Сообщения лежат в базе зашифрованными (AES-256-GCM), у каждого записан номер ключа, так что ключи можно менять, не перешифровывая старую историю.
Шифрование серверное, сквозного между устройствами нет.
Утечка дампа базы не раскрывает переписку.
06Реалтайм
WebSocket и пуши
Сообщения, реакции, звонки, упоминания и ответы поддержки приходят по WebSocket через Laravel Reverb в приватные каналы пользователя.
Когда вкладка закрыта, работает Web Push (VAPID): входящий звонок и личное сообщение приходят системным уведомлением.
Ответ поддержки виден сразу, без перезагрузки страницы.
07Надёжность
Звонок переживает сбои
Звонок восстанавливается после перезагрузки вкладки и перезапуска приложения. Присутствие держится на heartbeat, зависшие сессии убирает серверный крон.
Таймеры переподключения сами планируют следующий шаг и не зависят от перерисовок React. При деплое старые JS-чанки хранятся ещё 7 дней, чтобы открытая вкладка с идущим звонком не упала на ChunkLoadError.
Выкатка новой версии не обрывает текущие звонки.
08Windows
Десктоп-клиент на Electron
Автообновление скачивает только изменившиеся блоки установщика. Клиент видит запущенную игру и показывает её друзьям в статусе, отсеивая лаунчеры и античиты.
Пока идёт звонок, клиент поднимает приоритет своих процессов и отключает для них энергосбережение Windows (EcoQoS): иначе в полноэкранной игре система душит фон и голос начинает заикаться.
Голос не прерывается, даже когда игра на весь экран.
09Сеть в России
Разбор блокировки у провайдера
У части абонентов одного провайдера сайт открывался через раз. Разбирали по шагам: TCP до сервера проходит, TLS с SNI обрывается, без SNI работает, а соседние сайты из той же подсети /24 недоступны точно так же.
Вывод: провайдер фильтрует по подсети, наш сервер тут ни при чём. Сайт и десктоп-клиент перевели на HTTP/2, чтобы ходить одним TLS-соединением вместо шести.
Проверка из 40 точек по России (Globalping): сайт открылся в 39.
10Эта страница
Демо-вход в один клик
Кнопка «Открыть демо» создаёт временный аккаунт с готовым сервером, ролями, друзьями и перепиской. Переписка хранится зашифрованной, как у всех.
Гостю закрыто всё, через что можно написать реальным людям, у него своя квота на файлы, в статистику он не попадает, а через сутки его удаляет крон вместе с сервером и чатами.
Продукт можно пощупать без регистрации и никому при этом не помешать.
03 · качество
Как держим порядок
- Фронтенд разложен по Feature-Sliced Design: app, widgets, features, entities, shared.
- Бэкенд: маршрут → Form Request → контроллер → политика → сервис. Ожидаемые ошибки бросаются типизированными исключениями с единым форматом ответа.
- PHPStan (Larastan) 5-го уровня без baseline и Pint. CI на GitHub Actions: стиль, статический анализ, тесты.
- React Compiler со строгими правилами: без setState в эффектах и чтения ref во время рендера.
- Лимиты запросов именованные, у каждого свой счётчик: пинги не съедают лимит входа.
- Учитывается системная настройка «меньше движения»: анимации становятся спокойнее.
04 · стек
На чём написано
- Фронтенд
- Next.js 16React 19TypeScriptRedux Toolkit + RTK QueryTailwind CSS 4livekit-client
- Бэкенд
- PHP 8.4Laravel 12SanctumReverbMySQLRedis
- Медиа
- WebRTCLiveKit SFUcoturnDeepFilterNet · WASMRNNoise
- Десктоп
- Electron 37electron-builderelectron-updater
- Инфраструктура
- nginx · HTTP/2systemdпланировщик LaravelGitHub Actions
Лучше один раз потрогать
Демо открывается за пару секунд: свой сервер, друзья и переписка уже на месте. Через сутки всё удалится само.