Read in:
Русский

anatomy-15-months

title: Анатомия trip2g: 15 месяцев кода
free: true

trip2g — платформа для публикации Obsidian-заметок с подпиской и интеграцией с Telegram. Go-монолит + $mol-фронтенд + SQLite.

Через 15 месяцев разработки я решил посчитать, что внутри.

Собственный код

Слой Строк
Go (продакшн) ~32 000
Go (тесты) ~81 000
TypeScript ($mol UI) ~41 000
$mol шаблоны (.view.tree) ~5 000
SQL-миграции ~2 500 (120 файлов)
SQL-запросы ~2 600 (read: 1 476, write: 1 159)
GraphQL-схема 3 400
obsidian-sync плагин ~9 700
Итого ~177 000

Не считается: Go codegen (gqlgen, sqlc, moq) — 139 000 строк в 134 файлах. Vendor bundles (Tiptap и др.) — 190 000 строк.

1 615 коммитов за 15 месяцев

Codegen в 4 раза больше рукописного Go

SQL-запросы и GraphQL-схема пишутся вручную. Всё остальное — резолверы, слой доступа к БД, моки — генерируется. Это и есть смысл gqlgen + sqlc: описываешь намерение, инструменты делают бойлерплейт.

Админка: 38 страниц

Семь разделов, 38 страниц:

Раздел Страниц
system 9
content 7
users 5
integrations 5
monetization 4
seo 4
telegram 3

174 $mol-компонента только в админке.

E2E как главный гарант надёжности

Unit-тесты проверяют изолированную логику. Главная страховка — E2E-suite на Playwright: 29 spec-файлов, ~5 400 строк. Каждый прогон поднимает 4 изолированных инстанса приложения (основной + 3 peer для federation-тестов), MinIO и embedding-сервис на чистой SQLite-базе.

Порядок запуска не случаен: setup → CLI sync → sync к peers → основные браузерные тесты → CSS hot-reload → federation → webhooks. Webhooks идут последними намеренно — они ждут пустой очереди фоновых задач.

Telegram-тесты — отдельный opt-in режим (ENABLE_TG=1): используют реальные каналы, проверяют снапшоты сообщений до и после редактирования.

Крон, очереди и фоновые задачи

Всё работает в одном бинарнике — никаких отдельных воркеров, Redis или RabbitMQ.

Крон построен на robfig/cron. Задачи описаны в коде и хранятся в базе, поэтому их можно включать и отключать из админки без передеплоя. Типичные задачи: синхронизация подписчиков Patreon/Boosty, отправка запланированных Telegram-постов, очистка протухших токенов.

Очередь фоновых задач работает на goqite — job queue поверх SQLite. Короткие задачи (отправить одно Telegram-сообщение, обработать один webhook) попадают сюда. Очередь переживает перезапуски, потому что это просто таблица. Параллельно работают два бекенда: goqite для коротких задач и backlite для более длительных.

Telegram bot orchestration — одновременная обработка апдейтов от нескольких ботов. У каждого бота своя цепочка обработчиков: маршрутизация сообщений, state machine для многошаговых диалогов (flow привязки аккаунта, flow инвайта), управление доступом к subgraph. Состояние ботов хранится в SQLite и переживает перезапуски.

Telegram account orchestration использует gotd/td (чистый Go, без CGO) для публикации через реальные Telegram-аккаунты, а не ботов. Это обходит ограничения ботов на типы медиа и кастомные эмодзи. Каждый аккаунт — отдельная MTProto-сессия. Оркестратор управляет жизненным циклом сессий, переподключениями и обработкой flood-wait сразу для нескольких аккаунтов, публикующих в несколько каналов.

Интересные Go-зависимости

Контентный пайплайн

  • goldmark — Markdown-парсер с полноценным AST. Используется везде: рендеринг заметок, форматирование Telegram-постов, индексация для поиска, резолвинг wikilinks. AST позволяет обходить и трансформировать дерево документа вместо regex-патчинга HTML.
  • CloudyKit/jet — шаблонизатор с собственным AST и вычислителем выражений. Управляет JSON-лейаутами и template views. Мощнее html/template: переменные, функции, наследование, прямой доступ к AST. Подробнее: документация по Jet.
  • valyala/quicktemplate — компилируемые шаблоны для дефолтной темы сайта. Шаблоны становятся Go-кодом на этапе сборки — никакой рефлексии и парсинга в рантайме.
  • google/go-jsonnet — Jsonnet для конфига сайта. Поддерживает импорты, функции и вычисляемые значения — то, чего не умеют JSON/TOML.

Поиск

  • blevesearch/bleve — fulltext-поиск, встроенный в бинарник. Никакого Elasticsearch, никаких внешних процессов. Также обрабатывает векторный поиск для «похожих заметок» через OpenAI embeddings.

Хранилище

  • minio/minio-go — S3-совместимое хранилище для загруженных файлов и автоматических бекапов.
  • mattn/go-sqlite3 + modernc.org/sqlite — два SQLite-драйвера. CGO-версия для продакшна, pure-Go — для окружений без CGO.
  • maragu/goqite — очередь задач поверх SQLite. Одна внешняя зависимость меньше.

Транспорт

  • gotd/td — реализация TDLib на чистом Go (без CGO). Обеспечивает публикацию через Telegram userbot.
  • valyala/fasthttp — используется специально для GraphQL SSE subscriptions, где стриминг net/http имеет ограничения.

Утилиты

  • oklog/ulid — сортируемые ID (в отличие от UUID). Порядок вставки сохраняется в индексах.
  • bradleyjkemp/cupaloy — snapshot-тесты. Удобно для тестирования HTML-вывода рендерера.

70+ пакетов, 50+ use cases

internal/ организован по доменам. Каждый use case (internal/case/) — одна операция: один файл, одна функция, один Env-интерфейс только с нужными зависимостями.

Только для авторизации — 7 видов токенов: email-код, HAT, Telegram, purchase, personal, short API, user token. Каждый в своём пакете.

Монетизация: Patreon, Boosty, NOWPayments (крипто). Интеграции: импорт из GitHub, импорт из Notion, OpenAI embeddings, federation между инстансами trip2g, webhooks с парсингом agent response.

Ещё три пакета транслитерации Ru→Latin для URL slug'ов. Некоторые задачи имеют слои.

Нашли баг, пока делали этот лендинг

При разработке этой страницы обнаружили баг в обработке asset() в шаблонизаторе.

asset() в Jet-шаблонах возвращает presigned URL из MinIO для изображений. Эти URL содержат & между параметрами. Оказалось, что jet.NewSet() в Jet v6 по умолчанию ставит template.HTMLEscape в качестве эскейпера вывода — неочевидно по имени, которое звучит как «не-HTML-сет».

Результат: & в presigned URL превращался в &amp; внутри url() в CSS внутри тега <style>. HTML-декодирование внутри <style> не происходит, поэтому браузер отправлял MinIO строку &amp;X-Amz-Credential=... буквально — это ломало верификацию AWS-подписи. MinIO возвращал: The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.

Исправление: передавать jet.WithSafeWriter(nil) при создании Jet-сета для лейаутов. Лейауты содержат доверенный, заранее отрендеренный контент, созданный владельцем сайта, — не пользовательский ввод, — поэтому HTML-экранирование здесь не нужно и ломает URL.

Зачем этот отчёт

14 месяцев фулл-тайма в идеальных условиях. Свой язык, свой фронтенд-фреймворк, свой шаблонизатор. Протокол федерации. RAG. Telegram userbot-оркестрация. Семь видов токенов авторизации.

И внутренний голос «сделай ещё круче» всё ещё не насыщается. Всё — пора менять стратегию. Грубой силой это не вытащить. Некуда больше.

Что реально получилось: рантайм базы знаний. trip2g превращает Obsidian-хранилище в сайт, лендинг, вики, RAG-эндпоинт, MCP-сервер — доступный в публичном интернете или внутри частной сети. Протокол федерации позволяет строить domain-specific базы знаний и навигироваться между ними: свои ноды, друзья, компания, публичные хабы.

Текущая ставка: ценность в слое агента. Агент, который умеет вести Obsidian с одной стороны и выполнять усвоенные дисциплины с другой, — trip2g это персистентная память, которая делает агента связным между сессиями. База знаний — это второй мозг агента: она хранит то, что агент знает о себе, своём операторе и своей работе.

Спасибо Николаю Сенину за примеры сайтов на trip2g в продакшне. meditation.2pub.ru — автономный инстанс Hermes, который медитирует, ведёт сайт и Telegram-канал, генерирует музыкальные треки для главной страницы и готовится выкладывать это на YouTube.