Что это
Живой пример собран и работает - veyrax.ru. Здесь тот же код, вынутый в шаблон.
Запускается без ключей.MOCK_PROVIDERS=true - генерации идут в заглушку и наружу не уходит ничего, даже если ключи заданы. PAYMENT_PROVIDER=mock - полный цикл покупки подписки без реального мерчанта. Поднял через Docker, потыкал, понял устройство, и только потом подключаешь настоящие провайдеров.
Цена генерации выводится из себестоимости, а не задаётся руками. Себестоимость каждой модели лежит в cost-model.ts, курс и целевая маржа - в economics.ts, цена в кредитах считается из них. npm run economics возвращает ненулевой код, если хоть одна модель продаётся ниже целевой маржи, и тот же тест не даёт каталогу собраться. Держать цену и себестоимость двумя независимыми числами - это способ узнать об убытке из выписки, а не из кода.
Тёмная тема, лаймовый акцент, рукописный логотип. Весь бренд сидит на CSS-переменных --brand-* в одном файле, по коду цвета не разбросаны - под свой бренд перекрашивается быстро.
Админка с аналитикой и CRM к нему - отдельный сервис в этом же разделе.
Что внутри
- Лендинг: герой, галерея, тарифы, FAQ, блог, правовые страницы
- Кабинет: чат с моделями, видео, картинки, музыка, песни, озвучка, фотосессии
- Кредитная система на реестре: списание FIFO по лотам, сгорание подписочных кредитов, блокировка строки от гонок
- Конвейер генераций: атомарное списание, коллбэк и опрос одновременно, возврат кредитов при любом сбое
- Оплата: Тинькофф Merchant API v2, подписки с автопродлением, mock-провайдер для отладки
- Auth.js v5: пароль, вход через Яндекс, сброс пароля, подтверждение почты
- Экономика в коде: себестоимость моделей, целевая маржа, отчёт по воронке и точке безубыточности
- Хранилище S3-совместимое: MinIO локально, R2 или Yandex Object Storage в проде
- Prisma-миграции, сиды справочников, 9 файлов тестов на Vitest
- Docker-compose с postgres, minio и mailpit - вся инфраструктура одной командой
Что понадобится
- Node 22+, npm, Docker Desktop - больше для локального запуска ничего не нужно
- Next.js 16 (App Router), TypeScript strict, Tailwind CSS v4
- PostgreSQL 16 + Prisma 7, Auth.js v5
- Для боевого запуска: домен, эквайринг, S3-совместимое хранилище, ключи провайдеров
Ключи на старте не нужны: демо-режим включён по умолчанию.
Установка
Полная инструкция - в README.md внутри архива, деплой в проде - в DEPLOY.md. Там же CLAUDE.md и AGENTS.md: их видит Claude Code или Cursor, когда открываешь папку, и сразу понимает устройство проекта.
Быстрый старт
cp .env.example .env
npm install
npm run db:up # postgres + minio + mailpit в докере
npm run db:migrate # применить миграции
npm run db:seed # налить тарифы, модели, пресеты, FAQ, блог
npm run dev # http://localhost:3000
Секреты для локальной разработки генерируются так:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
и подставляются в AUTH_SECRET, PROVIDER_CALLBACK_SECRET, CRON_SECRET.
Что где крутится
| Сервис | Адрес |
|---|---|
| Приложение | http://localhost:3000 |
| Postgres | localhost:5433 |
| MinIO (консоль) | http://localhost:9001 |
| Mailpit (почта) | http://localhost:8025 |
Три вещи, которые стоит знать заранее
Демо-режим важнее ключей. ПриMOCK_PROVIDERS=true наружу не уходит ничего, даже если ключи прописаны. Сделано намеренно, чтобы случайно не потратить деньги. Чтобы проверить возврат кредитов, добавь в промпт слово «сломайся» - провайдер вернёт ошибку.
Баланс живёт в реестре, а не в поле. Источник правды - таблица CreditLedger, User.creditsCached это кэш. Меняешь кэш в обход реестра - получаешь расхождение, которое всплывёт через месяц и не сойдётся уже никогда.
Сетевого вызова внутри транзакции нет никогда. Списание держит блокировку строки пользователя до конца транзакции, и ответ провайдера подвесил бы баланс на всё время ожидания.
Перед боевым запуском
Перекрась бренд и замени тексты, прежде чем открывать сервис людям: шаблон приходит с моим оформлением, и два одинаковых сайта в выдаче никому не помогут. В docs/legal-todo.md лежит список того, что нужно закрыть по юридической части.
Ключей и паролей в архиве нет - только имена переменных окружения в .env.example и .env.prod.example.