Русский
anatomy-15-months
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 превращался в & внутри url() в CSS внутри тега <style>. HTML-декодирование внутри <style> не происходит, поэтому браузер отправлял MinIO строку &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.