Архитектура
Модульный монолит
Runne Pass — это единый Hono-сервис с чёткими границами модулей:
apps/api/src/
├── auth/ # регистрация, вход, JWT-сессии, отзыв
├── workspace/ # CRUD воркспейсов + владение
├── api-key/ # генерация/проверка ключей (HMAC-SHA256)
├── gateway/ # apiKeyAuth, rate limiting, CSRF
├── chat/ # /v1/chat/completions (в beta mock-провайдер)
├── payment/ # платежи YooKassa + вебхук
├── receipt/ # чеки 54-ФЗ
├── ledger/ # credit/debit/reservation/settlement/refund/adjustment
├── usage/ # оценка стоимости + отчёт по потреблению
├── quota/ # CRUD квот + применение
├── admin/ # дашборд + управление клиентами
└── cron/ # сверка, очистка orphan-резервирований, ретрай чеков, месячный сбросКаждый модуль экспонирует createXxxRouter() и слой сервисов. В beta всё работает одним процессом; модули выносятся в отдельные сервисы позже, при появлении сигналов масштабирования (после beta).
Стек
| Слой | Технология |
|---|---|
| Runtime | Bun 1.x |
| Framework | Hono 4.x |
| База данных | PostgreSQL 18+ (использует uuidv7()) |
| ORM | Drizzle |
| Кэш | Redis 7+ (rate limiting, denylist сессий) |
| Валидация | Zod |
| Monorepo | Bun workspaces + Turborepo |
| Тесты | Bun test runner |
Атомарные финансовые операции
Все движения средств выполняются внутри db.transaction с условными UPDATE … WHERE. Баланс обновляется, а строка реестра вставляется в одной транзакции, поэтому баланс и его история не могут разойтись.
- Резервирование —
UPDATE workspaces SET balance = balance - tokens WHERE balance >= tokens. Нет строки →402 insufficient_balance. - Кредит — безусловный
balance + tokens(от подтверждённого платежа). - Расчёт (settlement) — резерв уже сделан; расчёт пересчитывает фактическую стоимость против оценочной.
- Путь дефицита — если фактическая стоимость превышает резерв, разница списывается; если это не удаётся — воркспейс приостанавливается.
Реестр
transactions — это append-only журнал аудита. Каждое изменение баланса пишет строку с:
| Поле | Значение |
|---|---|
type | credit, debit, reservation, settlement, refund, adjustment |
amount | Дельта со знаком (отрицательная для дебетов/резервов/расчётов) |
balance_after | Снимок баланса после операции |
metadata | Контекст операции (id платежа, id usage-записи, id админа) |
Поскольку balance_after фиксируется в каждой строке, реестр восстанавливает историю баланса независимо от текущего баланса.
Идемпотентность
Идемпотентность важна там, где внешний актор может повторить вызов:
- Вебхук платежа — проверка статуса + условное обновление + уникальный индекс по
transactions.payment_id. - Отправка чека — стабильный ключ идемпотентности (id платежа) + атомарный claim (
pending/sending/succeededне отправляется повторно). - Инкремент потребления квоты — условный инкремент с гардом
NOT EXISTS, устойчивый к гонкам. - Месячный сброс квоты (cron) — идемпотентен по периоду.
Сверка
Cron-задача периодически пересчитывает баланс каждого воркспейса по строкам реестра и помечает расхождения. Это страховочная сеть: атомарные операции не должны расходиться, но сверка выявляет любую порчу данных или ручную правку.
Связанное
- Безопасность — как атомарность защищена от злоупотреблений.
- Ошибки — пути
402/429. - План развития — запланированные выносы.