UspAIChat: свой AI-чат на пяти провайдерах — с биллингом, мобильным клиентом и умным роутером
Кейс: self-hosted чат к Claude, GPT, Gemini, DeepSeek и Kimi. Один Node-процесс и SQLite вместо тяжёлой инфраструктуры, кредитный биллинг и роутер, который отправляет простые запросы в дешёвые модели.
Иметь один интерфейс ко всем моделям сразу — идея не новая. Непонятно другое: зачем ради неё тащить Postgres, Redis и оркестрацию контейнеров, если пользователей будет не миллион, а несколько сотен.
Этот кейс — про то, как выглядит AI-чат, собранный по обратному принципу: один Node-процесс и файл SQLite, никаких внешних сервисов в рантайме кроме самих API моделей. При этом внутри — пять провайдеров, четыре способа входа, кредитный биллинг с поштучным списанием за токены и мобильное приложение на Flutter.

Задача
Хотелось общаться с разными моделями — Claude, GPT, Gemini, DeepSeek, Kimi — не отдавая переписку в чужое облако: свои данные, свой сервер, своя биллинговая логика. Плюс раздать доступ команде и близким, контролируя расходы на токены.
Готовые open-source решения не подошли по двум причинам. Одни тянули за собой тяжёлую инфраструктуру, другие не умели нужного: четыре способа входа, кредитный биллинг и автовыбор модели. Отсюда — свой стек, максимально простой в эксплуатации.
Мотив здесь тот же, что и у любого self-hosted-решения: контроль над данными и предсказуемая стоимость. Где эта логика оправдана, а где превращается в дорогую паранойю, я разбирал в посте про локальные AI-модели — там же критерии, когда своё окупается.
Результат: ~17 000 строк кода — Express-бэкенд (3,5K), React-фронтенд (6,4K), Flutter-приложение (7,2K), задеплоенное на VPS и работающее в продакшене.
Стек
| Слой | Технологии |
|---|---|
| Backend | Node.js 20, Express, better-sqlite3 (WAL), JWT + refresh-токены |
| Web | React 18, TypeScript, Vite, Tailwind CSS, Zustand, framer-motion |
| Mobile | Flutter 3.29, Riverpod, GoRouter, Dio (SSE-стриминг) |
| AI | Anthropic SDK, OpenAI SDK (+ DeepSeek/Kimi через кастомный baseURL), Google Generative AI |
| Auth | Google OAuth, Apple Sign-In, Telegram-бот, email + пароль |
| Инфраструктура | VPS, Nginx, PM2, Certbot |
Архитектура
Классическая трёхзвенка с одним важным решением: SQLite вместо клиент-серверной СУБД. Для чата с сотнями пользователей better-sqlite3 в WAL-режиме даёт синхронный API без пула соединений, атомарные транзакции для биллинга и полнотекстовый поиск FTS5 — бесплатно, в одном файле.
flowchart LR
subgraph Clients["Клиенты"]
WEB["React SPA<br/>(Vite + Zustand)"]
MOB["Flutter App<br/>(Android / iOS)"]
end
subgraph Server["VPS · Nginx + PM2"]
NGINX["Nginx<br/>SSL, статика frontend/dist"]
API["Express API<br/>:3088"]
subgraph Services["Сервисы"]
ROUTER["autoRouter.js<br/>Smart Router"]
BILLING["billing.js<br/>кредиты, цены"]
TG["telegram.js<br/>бот-авторизация"]
end
DB[("SQLite (WAL)<br/>users · conversations ·<br/>messages · FTS5 · transactions")]
end
subgraph AI["AI-провайдеры"]
ANT["Anthropic"]
OAI["OpenAI"]
GEM["Google Gemini"]
DS["DeepSeek"]
KIMI["Kimi"]
end
WEB -->|"HTTPS / SSE"| NGINX --> API
MOB -->|"HTTPS / SSE"| NGINX
API --> ROUTER & BILLING & TG
API --> DB
ROUTER --> ANT & OAI & GEM & DS & KIMI
TG <-->|"polling"| TGAPI["Telegram Bot API"]
Главный экран веб-клиента — тёмная тема, закреплённые чаты, папки, горячие клавиши:

Схема данных
Ядро — четыре таблицы (пользователи, беседы, сообщения, транзакции), вокруг которых наросли фичи: шаринг, шаблоны промптов, платежи, промокоды, рефералка.
erDiagram
users ||--o{ conversations : "ведёт"
users ||--o{ refresh_tokens : "сессии"
users ||--o{ transactions : "биллинг"
users ||--o{ api_keys : "личные ключи"
users ||--o{ folders : "организация"
conversations ||--o{ messages : "содержит"
conversations ||--o| shared_conversations : "публичная ссылка"
messages ||--|| messages_fts : "FTS5-индекс"
users ||--o{ payments : "ЮKassa"
users ||--o{ promo_uses : "промокоды"
users {
text id PK "UUID"
text email UK "nullable для OAuth"
text google_id UK
text apple_id UK
integer telegram_id UK
text role "admin | user"
real balance "кредиты"
}
conversations {
text id PK
text provider
text model
text system_prompt
integer is_pinned
}
messages {
text id PK
text role
text content
integer token_count
text files "JSON-вложения"
}
transactions {
text type "topup | charge"
real amount
real balance_after
integer tokens
}
Первый зарегистрировавшийся пользователь автоматически становится админом — удобно для self-hosted: развернул, вошёл, владеешь.
Четыре способа входа
Регистрация — только через OAuth (Google, Apple, Telegram), email-форма работает как вход для тех, кто уже задал пароль в профиле. Это отсекает ботов и избавляет от флоу подтверждения почты.
Самый нестандартный поток — вход через Telegram-бота: сервер генерирует шестизначный код, пользователь отправляет его боту, фронтенд поллит статус.
sequenceDiagram
autonumber
participant U as Пользователь
participant F as Frontend
participant B as Backend
participant T as Telegram-бот
F->>B: POST /auth/telegram/init
B-->>F: { code: "123456", bot_username }
F-->>U: Показывает код и ссылку на бота
U->>T: Отправляет «123456» в чат боту
T->>B: Находит код, создаёт/находит пользователя
B->>B: status = confirmed
loop каждые 2 секунды
F->>B: GET /auth/telegram/poll/:code
end
B-->>F: { user, accessToken, refreshToken }
Токены: access — JWT на 15 минут, refresh — UUID на 30 дней в БД. Фронтенд обновляет пару каждые 12 минут в фоне.
SSE-стриминг: почему fetch, а не EventSource
Ответы моделей стримятся через Server-Sent Events. Нюанс: нативный EventSource не умеет передавать заголовок Authorization, поэтому клиент читает поток через fetch + ReadableStream с ручным парсингом строк data:. Во Flutter то же самое делает Dio с ResponseType.stream.
Один HTTP-запрос несёт всю телеметрию: выбранную роутером модель, цену, чанки текста, счётчик токенов и итоговое списание с баланса.
sequenceDiagram
autonumber
participant C as Клиент (fetch / Dio)
participant S as Express /chat/stream
participant R as Smart Router
participant P as AI-провайдер
participant DB as SQLite
C->>S: POST /api/chat/stream { messages, provider: "auto" }
S->>DB: Проверка баланса
S->>R: analyzePrompt(text)
R-->>S: { tier, model, экономия }
S-->>C: data: {type:"routing_info", tier:"SIMPLE", ...}
S-->>C: data: {type:"price", pricePer1k}
S->>P: Стриминговый запрос к API модели
S-->>C: data: {type:"start", message_id}
loop генерация
P-->>S: чанк текста
S-->>C: data: {type:"chunk", content}
end
S-->>C: data: {type:"tokens", count}
S->>DB: Атомарная транзакция: сообщение + списание кредитов
S-->>C: data: {type:"done", full_content, balance_after, cost}
Так это выглядит вживую — вопрос, потоковый ответ и плашка роутера с выбранной моделью и экономией:

Markdown-ответы рендерятся с подсветкой синтаксиса, нумерацией строк и кнопкой копирования:

Smart Router: −99% к цене на простых запросах
Фича, которой я горжусь больше всего. В режиме «Авто» бэкенд прогоняет промпт через 14-мерный эвристический классификатор: плотность кода, маркеры рассуждений, техническая терминология, многошаговость, креативность, ограничения формата, доменная специфика и так далее. Каждое измерение даёт вклад с весом, сумма — скор сложности от 0 до 1.
flowchart TD
P["Промпт пользователя"] --> A["Анализ по 14 измерениям<br/>код · рассуждения · термины ·<br/>многошаговость · креатив · формат · ..."]
A --> S{"Скор сложности"}
S -->|"< 0.3"| T1["SIMPLE<br/>GPT-4.1 Nano, Gemini Flash"]
S -->|"0.3 – 0.5"| T2["MEDIUM<br/>Gemini 2.5 Flash, GPT-4.1, Sonnet"]
S -->|"≥ 0.5"| T3["COMPLEX<br/>Claude Opus, o3, DeepSeek R1"]
T1 & T2 & T3 --> K{"Есть ключ<br/>провайдера?"}
K -->|"да"| M["Выбранная модель<br/>+ расчёт экономии vs Opus"]
K -->|"нет"| F["Следующий кандидат тира<br/>(fallback по всем тирам)"]
F --> K
Профиль пользователя сдвигает тир: eco понижает (экономия), premium повышает (качество). Классификатор — чистые регулярки и веса: ноль дополнительных API-вызовов и миллисекунды на решение.
На скриншотах выше видно результат: приветственный вопрос ушёл в GPT-4.1 Nano с плашкой «SIMPLE −99%» — вместо того чтобы жечь Opus по 25 кредитов за 1K токенов.
Логика здесь ровно та же, что и в ручном выборе модели под задачу — только автоматизированная. Если решаете это без роутера, помогут карта выбора модели под задачу и система 10-80-10, которая держит расходы на дорогих моделях в узде.
Биллинг
Кредитная система: админ пополняет баланс (или пользователь платит через ЮKassa либо Telegram-платежи), каждый ответ списывает кредиты по цене модели. Ключевые решения:
- Атомарность. Сообщение и списание — одна SQLite-транзакция; никаких «ответ есть, а деньги не списались».
- Приоритет ключей: личный ключ пользователя → глобальный ключ
__global__→ ошибка. Кто принёс свой ключ — платит провайдеру сам. - Админы не тарифицируются.
- Цены за 1K токенов настраиваются из админки, наценка ~×2,5 покрывает стоимость input-токенов.
Поиск, шаринг и остальной продуктовый слой
Полнотекстовый поиск — SQLite FTS5 по всем сообщениям с фолбэком на LIKE при невалидном FTS-запросе. Мгновенный, без Elasticsearch:

Из того, что накопилось вокруг ядра:
- Папки с drag-and-drop для организации бесед
- Шаблоны промптов — вставка по
/в поле ввода, глобальные шаблоны от админа - Шаринг беседы публичной ссылкой, опционально под паролем (bcrypt), со счётчиком просмотров
- Экспорт в Markdown / JSON / TXT
- Вложения: изображения (vision через base64), PDF, DOCX, кодовые файлы; до 10 файлов по 50 МБ
- Голос: распознавание через Web Speech API и озвучка ответов с очисткой markdown
- Системный промпт на уровне беседы + пресеты персонажей
- i18n: русский, английский, китайский
- Горячие клавиши: Ctrl+N, Ctrl+K, Ctrl+B, Ctrl+, — весь интерфейс доступен без мыши
API-ключи провайдеров живут в БД и управляются из интерфейса — в .env только секреты инфраструктуры:

Админ-панель
Статистика, управление пользователями (роли, блокировки, сброс пароля), глобальные API-ключи, пополнение балансов, история транзакций, редактор тарифов, промокоды:

Мобильное приложение
Flutter-клиент к тому же API — 7,2K строк Dart. Осознанные отличия от веба:
- Riverpod вместо BLoC — меньше бойлерплейта для проекта такого размера
- Нативные SDK Google/Apple Sign-In вместо веб-виджетов
- SSE через Dio
ResponseType.streamс ручным парсингом строк — тот же протокол, что и в вебе - Токены в
flutter_secure_storage, тёмная тема пиксель-в-пиксель повторяет веб-палитру - Telegram-вход через deep-link + поллинг — идентично вебу
Деплой и эксплуатация
git pull → cd frontend && npm run build → pm2 restart uspaichat
Nginx раздаёт собранную статику и проксирует /api на Node-процесс под PM2. SSL — Certbot. Вся база — один файл SQLite, бэкап = копирование файла. Миграции — идемпотентные ALTER TABLE в try/catch при старте: примитивно, но для одного файла БД надёжнее любого фреймворка миграций.
Цифры
| Метрика | Значение |
|---|---|
| Строк кода (свои) | ~17 000 |
| AI-провайдеров | 5 (≈25 моделей) |
| Способов входа | 4 |
| REST-эндпоинтов | ~60 в 17 роутерах |
| Внешних сервисов в рантайме | 0 (кроме AI API и платёжек) |
| Экономия Smart Router на простых запросах | до 99% |
Что я вынес из проекта
- SQLite недооценён. WAL-режим, FTS5 и синхронный better-sqlite3 закрывают потребности продукта на сотни пользователей — без единого контейнера.
- SSE поверх fetch — правильный компромисс: HTTP-семантика, работа через любые прокси и при этом полноценный заголовок
Authorization, которого нет уEventSource. - Эвристический роутер моделей окупается мгновенно: большинство реальных запросов — простые, и гонять их через премиум-модели просто дорого.
- Биллинг должен жить в транзакции. Любая пост-фактум сверка «ответили — списали» рано или поздно расходится.
Проект развивается: из последнего — платежи ЮKassa и Telegram, промокоды, реферальная программа, светлая тема.
Посмотреть вживую
Перейти на сайтapp.aifuturenow.ru Исходники на GitHubircitdev/UspAIChatЧитайте также
- Как объединить Claude, ChatGPT и Gemini в одну команду с общей памятью — та же идея мультипровайдерности, но без своего сервера.
- Какую модель под какую задачу: карта выбора на 2026 — что зашито в логику роутера, разложенное по полочкам.
- Локальные AI-модели: когда это защита данных, а когда дорогая паранойя — трезвые критерии, когда self-hosted оправдан.
- Docmost: корпоративная Wiki под полным контролем — ещё один self-hosted-инструмент из той же логики.








Комментарии
Войдите через Telegram, чтобы оставить комментарий:
Пока нет комментариев. Будьте первым.