# UspAIChat: свой AI-чат на пяти провайдерах — с биллингом, мобильным клиентом и умным роутером

> Кейс: self-hosted чат к Claude, GPT, Gemini, DeepSeek и Kimi. Один Node-процесс и SQLite вместо тяжёлой инфраструктуры, кредитный биллинг и роутер, который отправляет простые запросы в дешёвые модели.

Источник: https://blog.uspeshnyy.ru/blog/keys-uspaichat-selfhosted-ai-chat/
Автор: Александр Успешный · blog.uspeshnyy.ru
Опубликовано: 2026-07-21
Теги: кейс, self-hosted, система, продуктивность

---

Иметь один интерфейс ко всем моделям сразу — идея не новая. Непонятно другое: зачем ради неё тащить Postgres, Redis и оркестрацию контейнеров, если пользователей будет не миллион, а несколько сотен.

Этот кейс — про то, как выглядит AI-чат, собранный по обратному принципу: **один Node-процесс и файл SQLite**, никаких внешних сервисов в рантайме кроме самих API моделей. При этом внутри — пять провайдеров, четыре способа входа, кредитный биллинг с поштучным списанием за токены и мобильное приложение на Flutter.

![Экран входа UspAIChat](/images/posts/keys-uspaichat-selfhosted-ai-chat-auth.webp)

## Задача

Хотелось общаться с разными моделями — Claude, GPT, Gemini, DeepSeek, Kimi — не отдавая переписку в чужое облако: свои данные, свой сервер, своя биллинговая логика. Плюс раздать доступ команде и близким, контролируя расходы на токены.

Готовые open-source решения не подошли по двум причинам. Одни тянули за собой тяжёлую инфраструктуру, другие не умели нужного: четыре способа входа, кредитный биллинг и автовыбор модели. Отсюда — свой стек, максимально простой в эксплуатации.

Мотив здесь тот же, что и у любого self-hosted-решения: контроль над данными и предсказуемая стоимость. Где эта логика оправдана, а где превращается в дорогую паранойю, я разбирал в посте про [локальные AI-модели](/blog/lokalnye-ai-modeli-kogda-eto-zaschita-dannyh-a-kogda-dorogay/) — там же критерии, когда своё окупается.

**Результат:** ~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 — бесплатно, в одном файле.

Главный экран веб-клиента — тёмная тема, закреплённые чаты, папки, горячие клавиши:

![Главный экран](/images/posts/keys-uspaichat-selfhosted-ai-chat-main.webp)

## Схема данных

Ядро — четыре таблицы (пользователи, беседы, сообщения, транзакции), вокруг которых наросли фичи: шаринг, шаблоны промптов, платежи, промокоды, рефералка.

Первый зарегистрировавшийся пользователь автоматически становится админом — удобно для self-hosted: развернул, вошёл, владеешь.

## Четыре способа входа

Регистрация — только через OAuth (Google, Apple, Telegram), email-форма работает как вход для тех, кто уже задал пароль в профиле. Это отсекает ботов и избавляет от флоу подтверждения почты.

Самый нестандартный поток — вход через Telegram-бота: сервер генерирует шестизначный код, пользователь отправляет его боту, фронтенд поллит статус.

Токены: access — JWT на 15 минут, refresh — UUID на 30 дней в БД. Фронтенд обновляет пару каждые 12 минут в фоне.

## SSE-стриминг: почему fetch, а не EventSource

Ответы моделей стримятся через Server-Sent Events. Нюанс: нативный `EventSource` не умеет передавать заголовок `Authorization`, поэтому клиент читает поток через `fetch` + `ReadableStream` с ручным парсингом строк `data:`. Во Flutter то же самое делает Dio с `ResponseType.stream`.

Один HTTP-запрос несёт всю телеметрию: выбранную роутером модель, цену, чанки текста, счётчик токенов и итоговое списание с баланса. Зачем записывать это про каждый вызов и как ловить отказы, которые не выглядят как ошибка, — в разборе [как следить за AI-ботом в работе](/blog/kak-sledit-za-ai-botom-v-rabote/).

Так это выглядит вживую — вопрос, потоковый ответ и плашка роутера с выбранной моделью и экономией:

![Диалог с ответом модели](/images/posts/keys-uspaichat-selfhosted-ai-chat-chat.webp)

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

![Подсветка кода в ответе](/images/posts/keys-uspaichat-selfhosted-ai-chat-code.webp)

## Smart Router: −99% к цене на простых запросах

Фича, которой я горжусь больше всего. В режиме «Авто» бэкенд прогоняет промпт через **14-мерный эвристический классификатор**: плотность кода, маркеры рассуждений, техническая терминология, многошаговость, креативность, ограничения формата, доменная специфика и так далее. Каждое измерение даёт вклад с весом, сумма — скор сложности от 0 до 1.

Профиль пользователя сдвигает тир: `eco` понижает (экономия), `premium` повышает (качество). Классификатор — чистые регулярки и веса: ноль дополнительных API-вызовов и миллисекунды на решение.

На скриншотах выше видно результат: приветственный вопрос ушёл в GPT-4.1 Nano с плашкой «SIMPLE −99%» — вместо того чтобы жечь Opus по 25 кредитов за 1K токенов.

Логика здесь ровно та же, что и в ручном выборе модели под задачу — только автоматизированная. Если решаете это без роутера, помогут [карта выбора модели под задачу](/blog/kakuyu-model-pod-kakuyu-zadachu/) и [система 10-80-10](/blog/fable-5-bez-razoreniya/), которая держит расходы на дорогих моделях в узде.

## Биллинг

Кредитная система: админ пополняет баланс (или пользователь платит через ЮKassa либо Telegram-платежи), каждый ответ списывает кредиты по цене модели. Ключевые решения:

- **Атомарность.** Сообщение и списание — одна SQLite-транзакция; никаких «ответ есть, а деньги не списались».
- **Приоритет ключей:** личный ключ пользователя → глобальный ключ `__global__` → ошибка. Кто принёс свой ключ — платит провайдеру сам.
- **Админы не тарифицируются.**
- Цены за 1K токенов настраиваются из админки, наценка ~×2,5 покрывает стоимость input-токенов.

## Поиск, шаринг и остальной продуктовый слой

**Полнотекстовый поиск** — SQLite FTS5 по всем сообщениям с фолбэком на `LIKE` при невалидном FTS-запросе. Мгновенный, без Elasticsearch:

![Полнотекстовый поиск](/images/posts/keys-uspaichat-selfhosted-ai-chat-search.webp)

Из того, что накопилось вокруг ядра:

- **Папки** с 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-ключи](/images/posts/keys-uspaichat-selfhosted-ai-chat-settings.webp)

## Админ-панель

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

![Админ-панель](/images/posts/keys-uspaichat-selfhosted-ai-chat-admin.webp)

## Мобильное приложение

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% |

## Что я вынес из проекта

1. **SQLite недооценён.** WAL-режим, FTS5 и синхронный better-sqlite3 закрывают потребности продукта на сотни пользователей — без единого контейнера.
2. **SSE поверх fetch** — правильный компромисс: HTTP-семантика, работа через любые прокси и при этом полноценный заголовок `Authorization`, которого нет у `EventSource`.
3. **Эвристический роутер моделей** окупается мгновенно: большинство реальных запросов — простые, и гонять их через премиум-модели просто дорого.
4. **Биллинг должен жить в транзакции.** Любая пост-фактум сверка «ответили — списали» рано или поздно расходится.

Проект развивается: из последнего — платежи ЮKassa и Telegram, промокоды, реферальная программа, светлая тема.

## Посмотреть вживую

## Читайте также

- [Как объединить Claude, ChatGPT и Gemini в одну команду с общей памятью](/blog/kak-obedinit-claude-chatgpt-i-gemini-v-odnu-komandu-s-obsche/) — та же идея мультипровайдерности, но без своего сервера.
- [Какую модель под какую задачу: карта выбора на 2026](/blog/kakuyu-model-pod-kakuyu-zadachu/) — что зашито в логику роутера, разложенное по полочкам.
- [Локальные AI-модели: когда это защита данных, а когда дорогая паранойя](/blog/lokalnye-ai-modeli-kogda-eto-zaschita-dannyh-a-kogda-dorogay/) — трезвые критерии, когда self-hosted оправдан.
- [Docmost: корпоративная Wiki под полным контролем](/blog/docmost-korporativnaya-wiki-pod-polnym-kontrolem/) — ещё один self-hosted-инструмент из той же логики.
- [Публичный чат с одной нейросетью](/blog/keys-alisa-publichnyy-chat-yandexgpt/) — что происходит, когда такой же чат открываешь всем без регистрации.