Проект Lexis появился из конкретного раздражения: Duolingo учит заказывать яблоки, Memrise превратился в видеоплатформу, а ChatGPT не помнит, что пользователь уже разбирал Present Perfect в среду и снова путает его с Past Simple в пятницу. Автор хотел написать модели «давай сегодня про багтрекеры», получить чат на 15 минут, а в конце — три новых слова по уровню B1, которые потом всплывут в упражнениях. Такого продукта в публичном поиске не нашлось, и за месяц появился Lexis — репозиторий github.com/VDV001/lexis с MIT-лицензией.

Архитектурно приложение устроено как модульный монолит: четыре модуля с Clean Architecture внутри каждого (domain → usecase → handler → infra) и in-memory EventBus между ними. Шина передаёт события вроде WordLearned или SessionCompleted в модуль progress, не создавая прямых зависимостей между модулями. Интерфейс EventBus позволяет в будущем подменить его на Kafka без переписывания логики. Стек намеренно короткий: Go 1.26.1, chi для роутинга, PostgreSQL с pgx v5, Redis для blacklist-токенов и кеширования, sqlc для типобезопасного SQL без ORM.

ТехнологияВерсияНазначение
Go1.26.1Основной язык
chiv5.2.5HTTP-роутинг
PostgreSQL + pgxv5.9.1Основная БД
golang-migratev4.19.1Миграции, встроены в бинарь через embed.FS
Redisv9.18.0Blacklist-токенов и кеширование
sqlcТипобезопасный SQL без ORM
JWTv5.3.1Авторизация, симметричный HS256
zerolog + viperЛоги и конфигурация
testify + gomockv1.11.1Юнит-тесты

Первый технический якорь — pluggable AI-провайдеры. Каждый провайдер живёт в отдельном файле в пакете tutor/infra: claude_provider.go (6,2 КБ), openai_provider.go (6,6 КБ), gemini_provider.go (7,1 КБ) и qwen_provider.go (104 байта — честная заглушка). Все три рабочих провайдера реализуют единый интерфейс из трёх методов. Пользователь выбирает модель в настройках, фронт передаёт model_id в каждом запросе, handler достаёт провайдера из registry. Автор намеренно не добавлял в интерфейс специфические возможности вроде tool-calling — Gemini и OpenAI реализуют его по-разному, и эта сложность откладывается до момента, когда она реально понадобится.

Поддерживаются три рабочих LLM-провайдера: Claude (Anthropic), OpenAI и Gemini; Qwen пока заглушка в 104 байта.

Второй якорь — стриминг ответов через Server-Sent Events вместо WebSocket. Логика простая: AI-ответ идёт только в одну сторону, от сервера к клиенту. WebSocket для однонаправленного потока избыточен, требует отдельной обработки в nginx и написания логики реконнекта вручную. SSE работает поверх HTTP/2, браузер переподключается сам при разрыве через заголовок Last-Event-ID, а обработчик в Go занимает 30 строк против 100+ для WebSocket. Единственное ограничение — HTTP/1.1 допускает только 6 одновременных SSE-соединений на домен, но для одиночного приложения это несущественно.

Третий якорь — JWT с token rotation и reuse detection. Стандартные туториалы останавливаются на проверке подписи и таймстемпа, но это не закрывает сценарий утечки refresh-токена. В Lexis при логине пользователь получает access-токен на 15 минут и refresh-токен на 30 дней; refresh записывается в БД с полем family_id и флагом used = false. При обновлении бэкенд проверяет подпись, Redis-blacklist и флаг used. Если токен уже помечен как использованный — значит, кто-то его уже применил раньше. Реакция: RevokeAllForUser инвалидирует всю семью токенов, пользователь вылетает на логин на всех устройствах. Гонка между чтением и записью флага решается транзакцией с SELECT FOR UPDATE — без блокировки строки два одновременных запроса могут оба пройти проверку и оба получить новые токены. Весь файл auth_service.go занимает 230 строк.

Словарный модуль строится на алгоритме SM-2 — том же, что используется в Anki. Для каждого слова хранятся easiness_factor (по умолчанию 2.5), interval_days, repetitions и оценка quality от 0 до 5. Фоновая горутина с time.Ticker ежедневно пересчитывает количество слов к повторению и кеширует результат в Redis, чтобы не нагружать Postgres при каждом заходе в дашборд. Четыре режима тренировок — квиз с выбором из четырёх вариантов, текстовый перевод с оценкой от ИИ, заполнение пропусков и составление слова из перемешанных букв — обновляют SM-2 quality и двигают расписание повторений.