Skip to content

Архитектура

Модульный монолит

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).

Стек

СлойТехнология
RuntimeBun 1.x
FrameworkHono 4.x
База данныхPostgreSQL 18+ (использует uuidv7())
ORMDrizzle
КэшRedis 7+ (rate limiting, denylist сессий)
ВалидацияZod
MonorepoBun 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 журнал аудита. Каждое изменение баланса пишет строку с:

ПолеЗначение
typecredit, 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-задача периодически пересчитывает баланс каждого воркспейса по строкам реестра и помечает расхождения. Это страховочная сеть: атомарные операции не должны расходиться, но сверка выявляет любую порчу данных или ручную правку.

Связанное