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

03 — Compliance & Security

Compliance Map


Bank of Thailand Requirements

Payment Systems Act B.E. 2560 (PSA 2017)

The Act requires licensed payment service providers to ensure real-time settlement finality, full transaction traceability, and system resilience.

Requirement Implementation Where
Real-time settlement finality TigerBeetle atomic transfers — PENDING → POST is a single atomic operation. No intermediate states visible to users. TigerBeetle
Transaction traceability Every status change is recorded in pm.intent_event with timestamp, actor, and reason. Raw PSP responses stored in payload JSONB. pm.intent_event
Unique transaction reference Each intent has an immutable intentId (UUID) and an idempotencyKey. The trace_id = intent_id in every log line and OTLP span. PM + all logs
System resilience OutboxWorker crash recovery, Status Poller, TigerBeetle pending timeout, Redis Streams DLQ for external adapters. PM background workers
Audit log retention pm.intent_event and pm.tx_history are append-only. TigerBeetle transfers are physically immutable. PostgreSQL + TigerBeetle

AML / CTF — Anti-Money Laundering

Requirement Implementation Where
Transaction limits Daily and monthly caps enforced in the Limits Engine before funds are frozen. Limit exceeded → 429, no TB operation created. pm.payment_limit + Redis
User risk classification Auth Center passes metadata.tags (kyc_verified, vip, merchant_share, …) with every intent. Fee rules and limits use tagsInclude/tagsExclude to apply different rules to different user segments. Rule Engine + Limits Engine
Suspicious activity detection Limits Engine records running totals in Redis with day-level precision. Breaches generate structured log entries with intent_id and user_id for ops review. Phase 2C: dedicated reconciliation alerts table. Redis + pino structured logs
Full payment history pm.tx_history stores every debit, credit, and fee row with direction, amount, currency, timestamp, and intent reference. Queryable for any user, any time range. pm.tx_history

PDPA — Personal Data Protection Act

A critical architectural decision: PM contains zero PII.

Auth Center (Serverpod / public schema):
  ✓  Full name, phone number, national ID hash
  ✓  KYC documents (encrypted, Garage S3)
  ✓  Device fingerprints, biometric keys

Payment Manager (pm schema):
  ✓  userId (integer — an opaque reference)
  ✓  amounts, currencies, timestamps
  ✗  No names, no phone numbers, no documents

Why this matters for PDPA: - A PDPA erasure request only affects the public schema in Auth Center. PM data does not need to change. - A data breach in PM exposes transaction amounts and timestamps — not personal identifiable information. - KYC documents are stored encrypted in a separate Garage S3 bucket with independent access controls.

display name fields (fromName, toName, comment) in B6.1 are user-provided display labels — not resolved PII. They are passed by the caller (Auth Center) and treated as payment memo text.


Consumer Protection

Requirement Implementation
No duplicate charges idempotencyKey is unique per payment. Submitting the same key twice returns the original result — no second charge, no error.
Funds safety during failures TigerBeetle PENDING holds funds safely. They are only released on explicit POST (success) or VOID (failure/cancel). Auto-expire (300s timeout) applies to internal channels as a failsafe.
Transparent fee disclosure POST /intents/quote returns fee breakdown before the user commits. preFeeAmount and postFeeAmount are returned separately in every intent response so the app can show exactly who pays what.
Cancellation Intents in CREATED status can be cancelled by the user. Funds are only frozen at AUTHORIZED — no lock-in before confirmation.

Strong Customer Authentication (BOT / NatBank)

Requirement Status Implementation
PIN 6 digits ✅ Enforced Required at app launch, stored hashed, never transmitted
Biometric authentication ✅ Enforced Face ID / fingerprint for transaction confirmation
Auto-lock after 5 minutes idle ✅ Enforced Dart timer in Flutter; reactivation requires PIN re-entry
Device binding ✅ Enforced JWT tied to device key. Token on new device requires re-authentication
Short-lived JWT ✅ Enforced Access tokens expire in 15 minutes; refresh rotation prevents token theft

Security Architecture

HMAC Authentication — Service-to-Service

Every call into PM is signed. There are no "internal trusted" callers.

Signing string:
  X-Timestamp + "\n" + METHOD + "\n" + PATH + "\n" + sha256_hex(body)

Headers:
  X-Service-Id:  auth-center | nginx-gateway | miniapp-tours | ...
  X-Timestamp:   unix seconds (±60s window — replay protection)
  X-Signature:   hex(hmac_sha256(signing_string, service_secret))
  X-User-Id:     12345  (required for user-context calls)

Registered service principals:

Service ID Who Permissions
auth-center Serverpod All user payment types, userId required
nginx-gateway nginx Read-only (balance, history)
miniapp-{id} Merchant backend MINIAPP_CHARGE, MINIAPP_CREDIT for own merchant only
internal-pm PM cron jobs System movements, no X-User-Id

Secrets are stored hashed (bcrypt) in pm.service_key. Rotation = new row, old row deactivated.

Network Isolation

  • POST /intents is blocked at nginx on the public ingress — mini-app backends can only call it from the internal network
  • PSP adapters have no direct access to TigerBeetle — only through PM
  • Admin Panel has no direct access to PM's pm schema — only through Auth Center / PM API

Observability — Every Transaction is Traceable

trace_id = intent_id  (set at the start of POST /intents)

Propagation:
  Serverpod → PM:      X-Trace-Id header (W3C traceparent)
  PM → TigerBeetle:    user_data_128 = intentId (on every transfer)
  PM → Redis Streams:  trace_id field in every message
  PM → logs:           intent_id in every pino log line

Export: OTLP → Jaeger / Grafana Tempo
  Full chain visible in one trace:
  Flutter request → Auth Center → PM → TigerBeetle → IPPS → OutboxWorker