Architecture
Modular monolith
Runne Pass is a single Hono service with clear module boundaries:
apps/api/src/
├── auth/ # registration, login, JWT sessions, revocation
├── workspace/ # workspace CRUD + ownership
├── api-key/ # key generation/verification (HMAC-SHA256)
├── gateway/ # apiKeyAuth, rate limiting, CSRF
├── chat/ # /v1/chat/completions (mock provider in beta)
├── payment/ # YooKassa payments + webhook
├── receipt/ # 54-ФЗ receipts
├── ledger/ # credit/debit/reservation/settlement/refund/adjustment
├── usage/ # cost estimation + usage report
├── quota/ # quota CRUD + enforcement
├── admin/ # dashboard + customer management
└── cron/ # reconciliation, orphan cleanup, receipt retry, monthly resetEach module exposes a createXxxRouter() and service layer. Beta ships as one process; modules are extracted to services later if scaling signals appear (post-beta).
Stack
| Layer | Technology |
|---|---|
| Runtime | Bun 1.x |
| Framework | Hono 4.x |
| Database | PostgreSQL 18+ (uses uuidv7()) |
| ORM | Drizzle |
| Cache | Redis 7+ (rate limiting, session denylist) |
| Validation | Zod |
| Monorepo | Bun workspaces + Turborepo |
| Tests | Bun test runner |
Atomic financial operations
All money movements run inside db.transaction with conditional UPDATE … WHERE statements. The balance is updated and the ledger row inserted in the same transaction, so the balance and its history can never diverge.
- Reservation —
UPDATE workspaces SET balance = balance - tokens WHERE balance >= tokens. No row →402 insufficient_balance. - Credit — unconditional
balance + tokens(from a verified payment). - Settlement — reserve was already made; settle recomputes actual vs. estimated.
- Deficit path — if actual cost exceeds the reservation, the difference is debited; if that fails, the workspace is suspended.
The ledger
transactions is an append-only audit trail. Every balance change writes a row with:
| Field | Meaning |
|---|---|
type | credit, debit, reservation, settlement, refund, adjustment |
amount | Signed delta (negative for debits/reservations/settlements) |
balance_after | Snapshot of the balance after the operation |
metadata | Operation-specific context (payment id, usage record id, admin id) |
Because balance_after is captured on every row, the ledger reconstructs the balance history independently of the current balance.
Idempotency
Idempotency matters where an external actor can retry:
- Payment webhook — status check + conditional update + unique index on
transactions.payment_id. - Receipt send — stable idempotence key (payment id) + atomic claim (
pending/sending/succeededwon't re-send). - Quota usage increment — conditional increment with a
NOT EXISTSguard, race-safe. - Monthly quota reset cron — idempotent per period.
Reconciliation
A cron job periodically recomputes each workspace's balance from its ledger rows and flags mismatches. It is a safety net: the atomic operations should never diverge, but reconciliation detects any corruption or manual edit.