Возвращение в прошлое, которого у нас ещё не было

Интернет когда-то выглядел иначе. До бесконечных лент, рекомендательных алгоритмов и личных брендов существовали BBS, локальные сети, IRC, форумы и имиджборды. Пользователь приходил туда не ради собственного профиля, а ради темы, разговора или случайной встречи с незнакомым человеком.
У этих систем было много ограничений. Соединение могло быть медленным, интерфейс — текстовым, а навигация — целиком клавиатурной. Но именно ограничения часто делали общение более прямым. На экране не было десятка уведомлений, автовоспроизводящегося видео и алгоритма, который решал, что показать следующим.
Я сделал попытку вернуть это ощущение, не копируя технические проблемы
прошлого, написав публичную анонимная борду, основной клиент которой работает прямо
в современном терминале.
Проект открытый, каждый может развернуть его на своем сервере и погрузиться в ностальгию. Так же он развернут на моем VPS — все подробности в статье, ссылка на исходный код прикреплена.
Midnight Commander стал языком интерфейса
Внешне Bord напоминает Midnight Commander. Слева находится список досок, справа — каталог тредов или посты открытого обсуждения.
Двухпанельный интерфейс хорошо подходит для иерархии «доска → тред → пост». Пользователь одновременно видит контекст и содержимое, а основные действия всегда остаются на экране.
TUI поддерживает:
управление клавиатурой и мышью;
полноэкранную адаптивную компоновку;
узкие и широкие терминалы;
темы
classic,amber,greenиmono;поиск по каталогу и открытому треду;
просмотр длинного поста с прокруткой;
JPEG- и PNG-вложения;
inline-графику в совместимых терминалах;
открытие изображения системным приложением как fallback.
Скачать клиент можно на bord.pw. После распаковки он подключается одной командой:
bord --server https://bord.pw
Есть сборки для Linux, macOS и современного Windows Terminal на архитектурах AMD64 и ARM64.
На чём написан Bord
Проект реализован как Go-монорепозиторий и собирается в два основных бинарника:
bord— терминальный клиент;bord-server— публичный API, хранилище и операторские команды.
Go выбран не только ради производительности. Он позволяет собирать автономные бинарники для разных операционных систем, удобно работать с конкурентными соединениями и обходиться без большого runtime-окружения на сервере
Клиент построен на экосистеме Charm:
Bubble Tea управляет состоянием TUI по архитектуре Model–Update–View;
Bubbles предоставляет готовые интерактивные компоненты, включая редактор;
Lip Gloss отвечает за цвета, размеры и оформление;
x/ansi помогает безопасно работать с шириной Unicode-текста и переносами.
Backend использует стандартный HTTP-стек Go, сгенерированные адаптеры OpenAPI и SQLite через pure-Go драйвер modernc.org/sqlite. Для сборки не нужна системная библиотека SQLite или C-компилятор.
Как устроен TUI
Bubble Tea основан на однонаправленном потоке данных. В упрощённом виде цикл выглядит так:
событие клавиатуры, мыши или сети
↓
Update
↓
новое состояние модели
↓
View
↓
новый кадр терминала
Модель хранит текущий экран, активную панель, выбранную доску, тред, пост, позиции прокрутки, состояние загрузки и черновик редактора. Сетевые запросы выполняются как команды Bubble Tea и возвращают типизированные сообщения обратно в цикл обновления.
Такой подход отделяет ввод от отображения. Один и тот же переход состояния можно вызвать клавишей, кликом по нижней панели или мышью. Поэтому мышь не имеет отдельной «параллельной» логики: она использует те же операции модели, что и клавиатура.
Текст перед выводом проходит санитарную обработку. Управляющие последовательности из пользовательской публикации не должны превращаться в команды терминалу, менять заголовок окна или обращаться к буферу обмена. Дополнительно рендер учитывает широкие символы, emoji и комбинируемые Unicode- последовательности.
HTTP хранит состояние, SSE сообщает об изменениях
Bord разделяет получение данных и уведомления:
обычный HTTP API является источником истины;
Server-Sent Events сообщает клиенту, что состояние изменилось;
после события клиент повторно читает нужный каталог или тред через HTTP.
Это проще и надёжнее, чем передавать полную модель данных внутри непрерывного потока. Если SSE-соединение оборвалось, клиент переподключается и использует Last-Event-ID. Если история событий уже недоступна, сервер сообщает о reset, после чего клиент заново синхронизирует состояние.
На reverse proxy SSE вынесен в отдельную цепочку без буферизующего сжатия. Иначе небольшие события могли бы задерживаться до накопления достаточного объёма данных, и живое обновление перестало бы быть живым.
SQLite тоже осознанный выбор
Для централизованного MVP используется SQLite в режиме WAL. У такого решения есть практические преимущества:
один файл базы данных;
транзакции и внешние ключи;
простое резервное копирование;
отсутствие отдельного сервера СУБД;
предсказуемое обслуживание небольшого публичного проекта.
Изображения не складываются внутрь базы. SQLite хранит метаданные, состояния и ссылки, а нормализованные blobs находятся в файловом хранилище. Запись файла проходит через временный файл и атомарное перемещение, чтобы незавершённая загрузка не стала видимым вложением.
Репозиторий содержит миграции, проверку версии схемы, команды status/migrate, согласованное резервное копирование базы и blobs, а также проверку восстановления по SHA-256.
Анонимность
У Bord нет пользовательских аккаунтов, но это не означает, что сервер вообще не обрабатывает технические данные. Публичному сервису всё равно нужны защита от спама, rate limits, блокировки и расследование злоупотреблений.
Поэтому адрес клиента по умолчанию преобразуется в HMAC-SHA-256 индекс с серверным ключом. Такой индекс позволяет обнаруживать повторные злоупотребления, не сохраняя IP открытым текстом. Опциональное хранение исходного IP выключено; если оператор сознательно включает его, значение шифруется AES-256-GCM отдельным ключом и подпадает под ограниченный срок хранения.
Trusted proxy boundary настроена явно. Backend принимает forwarded-адрес только от известного reverse proxy, поэтому произвольный клиент не может просто подставить чужой X-Forwarded-For.
Сам клиент создаёт локальный случайный session secret. Для каждой публикации он также хранит delete capability — секрет, позволяющий позднее удалить собственный пост без аккаунта. Сервер получает стойкий verifier, а исходный delete secret остаётся в локальном конфигурационном файле пользователя с ограниченными правами.
Такая модель не делает человека невидимым для сети или закона. Она решает более конкретную задачу: сервису не требуется собирать профили, email и пароли только ради публикации текста.
Безопасная работа с изображениями
Загрузка файла — одна из самых опасных частей публичного сервиса. Bord принимает только JPEG и PNG, ограничивает размер тела запроса и параметры изображения, декодирует файл и создаёт нормализованную серверную копию.
Клиент никогда не строит shell-команду из имени вложения. Для внешнего просмотра используется системный opener с отдельными аргументами. HTTP-ответы изображений получают безопасные заголовки и предсказуемый Content-Type.
Контракт вместо JSON
Публичное API описано в api/openapi.yaml. Из контракта генерируется Go-клиент, а CI проверяет, что generated-код не расходится с исходной спецификацией.
Запросы на запись используют строгий JSON: неизвестные поля отклоняются. Для создания тредов и постов предусмотрен Idempotency-Key. Если сеть оборвалась после отправки, клиент может безопасно повторить запрос с тем же ключом, не создавая дубликат.
Отдельный management API предназначен только для операторов и не публикуется через внешний Caddy. Действия модераторов записываются в audit log.
Как проект развёрнут
Публичный экземпляр работает за Caddy с автоматическим TLS-сертификатом Let's Encrypt. Внешний reverse proxy принимает https://bord.pw, добавляет проверенные forwarded-заголовки и передаёт запрос во внутреннюю Docker-сеть.
Backend и внутренний edge не публикуют собственные порты в интернет. Для сайта включены HTTPS redirect, HSTS, Content Security Policy, запрет framing, nosniff и строгая referrer policy.
Сам landing полностью статический: HTML, CSS и небольшой JavaScript без frontend-фреймворка и внешнего CDN. Он показывает возможности проекта, выбирает архив для ОС и архитектуры и формирует команды установки.
Для альтернативного развёртывания в репозитории также есть systemd unit, Caddyfile, инструкции по установке, backup, restore, ротации ключей, retention и диагностике.
Выпуск
Проект собирается для шести клиентских платформ:
linux/amd64 linux/arm64
darwin/amd64 darwin/arm64
windows/amd64 windows/arm64
Release pipeline создаёт детерминированные .tar.gz и .zip архивы. Каждый архив содержит бинарник, инструкцию, MIT License и third-party notices. Рядом публикуется SHA256SUMS.
Возвращение в прошлое
Bord не пытается доказать, что старый интернет во всём был лучше. В нём было меньше удобства, меньше доступности и гораздо меньше встроенной безопасности.
Но у него была ценная идея: интерфейс может отойти в сторону и оставить человеку текст, тему и собеседников. Терминал хорошо напоминает об этом. Он ограничивает визуальный шум, поощряет клавиатурную навигацию и не пытается превратить каждое действие в метрику вовлечённости.
Под ретро-интерфейсом не обязана находиться ретро-архитектура. Там могут быть типизированные контракты, безопасная обработка медиа, шифрование, audit, идемпотентность, автоматические тесты и воспроизводимые релизы.
В этом и состоит идея Bord: вернуться не к старым ограничениям, а к старому ощущению прямого, необязательного и немного случайного разговора — используя инструменты, которых тогда ещё не существовало.
Попробовать: https://bord.pw
Исходный код: https://github.com/pavel-up-lab/bord
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.