Перейти к содержанию

01 — System Architecture

System Architecture


Service Layers

1. Client Layer

Client How it connects Purpose
Flutter App HTTPS → nginx → Auth Center Mobile wallet — all user actions
Admin Panel (SvelteKit) HTTPS → nginx → Auth Center Fee rules, user management, monitoring
Mini-App Backends HMAC → PM directly (internal network) Merchant charges/credits — Phase 2A

Key point: Flutter never talks to the Payment Manager directly. nginx enforces JWT validation on every request before it reaches PM.


2. Public Gateway — nginx

nginx is the only public entry point. It does two things:

  • Routing: /api/pm/* requests (balance, history) go directly to PM after JWT validation. All other requests go to Auth Center.
  • Auth guard: auth_request sub-request to Serverpod validates the JWT and injects X-User-Id header before forwarding. PM never trusts the client's claimed identity.

3. Auth Center (Serverpod / Dart)

Remains responsible for everything identity-related:

  • JWT issuance and refresh
  • KYC verification workflow
  • PII storage (names, phone numbers, documents)
  • Device binding (biometrics + PIN)
  • Push notifications
  • Mini-App catalog

What Auth Center no longer does: payment logic, ledger operations, balance storage. These moved entirely to PM.

Auth Center calls PM via POST /intents with HMAC authentication and the user's ID in the X-User-Id header.


4. Payment Manager (Node.js / Fastify)

The new standalone payment service. All financial logic lives here.

Internal modules

Module Responsibility
HMAC Middleware Validates every incoming request — no trusted callers
Router Maps (operationType, amount) to a channel name via pm.payment_route table
Saga Orchestrator Drives the intent state machine: CREATED → VALIDATED → AUTHORIZED → SETTLED
Rule Engine (CEL) Evaluates fee rules — PRE fees (sender pays) and POST fees (recipient pays)
Limits Engine Enforces daily/monthly transaction caps per user, cached in Redis
Channels Pluggable payment channel implementations (see below)
Outbox Worker Background loop — finalises async payments, writes history, notifies clients
Status Poller Crash recovery — detects stuck payments and resumes them

Channel architecture

Adding a new payment method = creating one new channel file. The core Saga, Rule Engine, and Limits Engine never change.

Channel Operation types Settlement
INTERNAL_P2P P2P_TRANSFER Instant (same HTTP request)
IPPS_TRANSFER IPPS_WITHDRAWAL, THAI_QR_PAY Async — OutboxWorker
QP_TRANSFER (Phase 2B) QP_TOPUP Async via Redis Streams
future VISA_DIRECT, SWIFT, MINIAPP_* Pluggable

5. TigerBeetle — Financial Ledger

TigerBeetle is a purpose-built financial database. It replaces blnkfinance as the source of truth for all balances and transfers.

Why TigerBeetle:

Property What it means
Atomic linked transfers A P2P transfer of sender → transit → recipient either fully succeeds or fully fails. No partial state.
Immutable append-only No UPDATE or DELETE on transfers. The ledger history is permanent.
Two-phase transfers Funds can be frozen (PENDING) then confirmed (POST) or cancelled (VOID). Perfect for async PSP flows.
Deterministic account IDs Account ID = uuidv5("onewallet-tb", account_name). IDs are verifiable at any time without extra state.
transit.balance = 0 invariant Every batch of linked transfers is balanced. The transit account always returns to zero after settlement — enforced mathematically by TB.

6. Data Layer

Store What lives there
PostgreSQL pm schema Intent records, fee rules, payment routes, transaction history, outbox events, PSP mapping, account balance snapshots
PostgreSQL public schema Users, profiles, KYC, devices — Serverpod only, PM never writes here
Redis / Valkey Pub/sub for real-time intent status (intent.{id}), Redis Streams for PSP job queues (Phase 2B), daily limits cache

Key Design Decisions

Single entry point — POST /intents

Every payment from every caller goes through one endpoint with one auth scheme. No special internal routes, no bypass paths.

HMAC for all service-to-service calls

Every caller — Auth Center, nginx gateway, mini-app backends — must sign requests with a registered secret. The signing string includes timestamp, method, path, and body hash, preventing replay attacks.

PII never reaches PM

PM receives only userId (an integer). Names, phone numbers, and KYC data stay in Serverpod's public schema. This limits PDPA exposure and simplifies the payment schema.

metadata.tags — risk classification without cross-schema reads

Auth Center passes user context (e.g., ["kyc_verified", "vip"]) in the metadata.tags field of POST /intents. PM uses these tags to apply the correct fee rules and limits without ever reading from public.*.