TechTok — Android-приложение, которое показывает новости технологий и науки лентой, как в TikTok: одна карточка на экран, свайп вверх — следующая. За лентой стоит serverless-конвейер на AWS. Каждый час он забирает свежие статьи из RSS, прогоняет их через LLM и раскладывает по четырём языкам. Ниже — как это устроено, сколько стоит и какие грабли попались по дороге. Код открыт под MIT: github.com/tormozz48/techtok.
Что умеет приложение
-
Карточка: картинка, заголовок, 2–3 предложения сути, строка «почему это важно», источник и тема.
-
Тап по карточке открывает компактную версию статьи на 400–600 слов с иллюстрациями из оригинала и ссылкой на него.
-
Карточки, пересказы и интерфейс — на английском, русском, украинском и польском.
-
8 тем (ИИ, разработка, гаджеты, стартапы, безопасность, наука, космос, биология) и 11 источников: Hacker News, The Verge, Ars Technica, TechCrunch, ScienceDaily, Phys.org, Quanta Magazine, Nature News, arXiv cs.AI, GitHub Blog и Hugging Face Blog.
-
Закладки, история, поиск по ним, озвучка и статистика чтения.
Конвейер
Раз в час EventBridge запускает машину состояний Step Functions. Она загружает список источников и в Map с параллельностью 4 забирает каждую ленту условным GET. У каждого элемента Map свой catch, так что одна сломанная лента не валит весь прогон. Дубликаты отсекаются по канонизированному URL (unique(canonical_url) в таблице posts), дальше уходят только новые статьи.
Затем три очереди SQS, у каждой своя DLQ:
-
Transform скачивает страницу (с учётом robots.txt, не дольше 10 секунд и не больше 2 МБ), архивирует HTML в S3, вытаскивает текст и картинку, зеркалирует картинку в CloudFront (всё меньше 600 px отбрасывается) и одним вызовом LLM пишет карточку и определяет тему.
-
Translate переводит карточку на остальные три языка. В том же вызове модель сама проверяет свой перевод.
-
Content пишет компактную статью, по одному заданию на каждый из четырёх языков, и кладёт JSON в S3 за CloudFront.
Главное правило конвейера — разделение отказов. Если проблема в контенте (пейволл, пустая страница, мусорный ответ модели), конвейер деградирует: пользователь увидит карточку из RSS-выдержки, и лента не опустеет. Если проблема в инфраструктуре (база недоступна, таймаут), Lambda бросает исключение: SQS повторяет попытку, потом сообщение уходит в DLQ, и срабатывает алерт. Путать ветки нельзя. Деградация на инфраструктурной ошибке тихо превратит ленту в выдержки, а исключение на контентной будет бесконечно гонять одну и ту же статью.
LLM: всё заранее, ничего на запросе
Модель — google/gemini-3.1-flash-lite через OpenRouter: $0,25 за миллион входных токенов и $1,50 за миллион выходных. Bedrock остался подключённым как запасной провайдер и включается переменной окружения.
Раньше пересказ генерировался по тапу, и читалка ждала, пока задача завершится. Теперь всё делается сразу после загрузки статьи: карточка, три перевода и четыре пересказа. GET /v1/posts/{id}/content — это обычное чтение из S3, модель на пути запроса не участвует. Если пересказа ещё нет, API так и отвечает: available: false.
По токенам карточка — около 1,4 тыс. на входе и 250 на выходе, пересказ — около 3,5 тыс. и 1 тыс. Вместе с переводами выходит примерно цент на статью.
DynamoDB → Postgres
Первые версии жили на DynamoDB. В августе 2026 она стоила $2,75 в месяц, и $2,65 из них приходилось на записи. Причина — усиление записи: оба GSI проецировали элемент целиком (ProjectionType: ALL), а 65% элемента занимали переводы. Каждое обновление поста записывалось трижды: в таблицу и в оба индекса. Лечение — проекции INCLUDE и KEYS_ONLY плюс двухшаговая лента: ранжировать узких кандидатов, а полные данные догружать только для страницы, которая уйдёт в ответ.
Переезд на Neon Postgres с Drizzle случился уже не из-за денег. Нужны были:
-
запросы, под которые не надо заранее проектировать ключи для каждого паттерна доступа;
-
инварианты, которые проверяет сама база: внешние ключи с явным
on delete, уникальность,generated always as identity; -
юнит-тесты на настоящем Postgres через PGlite, без моков.
Схема — 16 нормализованных таблиц. Новое узкое место — исходящий трафик: у Neon лимит 5 ГБ в месяц, а каждая строка теперь идёт через интернет. Поэтому двухшаговую ленту сохранили. Кандидаты выбираются по частичному индексу (primary_topic_id, published_at desc, id desc), а перевод догружается только на языке пользователя. По оценке это около 48 КБ трафика из базы на запрос ленты против ~130 КБ без языкового фильтра.
Мелочь про безопасность: после перехода на целочисленные id ключи объектов в CDN перестали строиться из id поста, такие легко перебрать. Ключ — sha-256 от канонического URL статьи.
Мобильное приложение
Expo, React Native, expo-router и React Native Paper (Material 3). Вход через Google: ID-токен проверяет JWT-авторизатор API Gateway ещё до Lambda. Подписка через Google Play Billing (expo-iap), покупку сервер проверяет в Play Developer API.
Грабли, которые стоили времени:
-
expo prebuildради нового нативного модуля запускать нельзя. Проектandroid/закоммичен, и prebuild откатывает все ручные правки, а заодно сбрасываетversionCodeв 1. -
В Play Console нет кнопки создания подписки, пока не загружена сборка с разрешением
com.android.vending.BILLING. Поэтому сначала выходит мобильная часть и только потом настраивается продукт, хотя логичным кажется обратный порядок. -
Цены в Play задаются без НДС. Налог страны Play добавляет сверху, так что €2,99 в Австрии превращаются в €3,59.
-
Каждый
versionCodePlay принимает один раз. Поэтому CI одной правкой (edit) кладёт сборку сразу во внутренний трек и в закрытый тест.
Как это разрабатывается
Проект делает один человек, поэтому главный инструмент — журнал решений: в docs/DESIGN.md 116 записей, у каждой есть обоснование и условие, при котором решение стоит пересмотреть. Много кода пишется в паре с Claude Code, и журнал не даёт агенту заново решать то, что уже решено.
Спорное правило: комментариев в коде нет совсем. «Что» выражается именами, «почему» живёт в журнале решений и в сообщениях коммитов. Правило проверяет скрипт на сканере TypeScript в pnpm lint, а хук блокирует правку в момент записи.
CI: на каждый PR — линтер, типы и тесты (Vitest и jest-expo), без доступа к AWS. После мержа в main — деплой на dev, E2E-тесты на живом dev-стенде через OIDC, затем production. Параллельно собирается Android-сборка и уходит в Google Play.
Во что обходится
По Cost Explorer за август весь AWS (dev и production вместе) стоил $11,08 в месяц, самой крупной строкой был CloudWatch. С тех пор из счёта ушли DynamoDB, дашборд CloudWatch и часть алармов. LLM — около цента на статью, OpenRouter выставляет счёт отдельно.
Бесплатный тариф — 30 карточек и 10 полных статей в день. Plus снимает лимиты и стоит €2,99 в месяц или €24,99 в год (без НДС).
12 тестировщиков
Остался последний шаг — Google Play. Приложения с новых личных аккаунтов разработчиков попадают в магазин только после закрытого теста: минимум 12 человек, 14 дней подряд.
Тестировщиков удобно собирать через обычную Google-группу: вступить может любой, список участников видит только владелец, писать в группу нельзя. Play Developer API для треков и так принимает только группы, а в консоли списки адресов и группы — взаимоисключающие варианты.
И здесь нужна ваша помощь. Если у вас Android:
-
Вступите в группу тестировщиков тем аккаунтом Google, что на телефоне.
-
Откройте приглашение и нажмите «Стать тестировщиком».
-
Установите TechTok из Google Play и заглядывайте в него хотя бы раз в пару дней.
Все шаги с QR-кодами — на techtokapp.eu/ru/test. Тестировщики получают Plus бесплатно. Баги, кривые переводы и идеи — в комментарии или на support@techtokapp.eu.
ссылка на оригинал статьи https://habr.com/ru/articles/1087106/