AlmaPay Backend
Bank-grade remittance API for UK → East Africa corridors
A production-oriented remittance backend for secure transfers from the UK to East Africa (KES, UGX, TZS, SOS). Built around an immutable ledger, strict transaction state machine, idempotent settlement, and compliance-first admin controls — with product UI mockups shown for context.
Phone screens are product UI mockups for the remittance experience. The shipped engineering work is the Node.js / Express / PostgreSQL backend: ledger, state machine, settlement, KYC/AML, and admin APIs.



Problem
Cross-border remittance backends fail in the gaps — race conditions on balances, duplicate payouts after retries, unclear transaction finality, and weak audit trails when compliance asks hard questions. Moving money from the UK to East Africa requires a system that stays correct under concurrency, crashes, and chargebacks.
Business context
AlmaPay targets remittance corridors into East Africa (KES, UGX, TZS, SOS). The product must satisfy operators (retry, refund, freeze), compliance (KYC/AML, 7-year audit retention), and customers (predictable fees, receipts, and status) without treating the database balance field as a source of truth.
Research
Modeled money movement as an immutable ledger with debit/credit invariants, separated economic finality (funds moved) from legal finality (chargeback window closed), and designed an explicit state machine with compensation paths for partial failures. Mapped RBAC to real ops roles: SUPPORT (read-only), OPS (retry/refund), SUPER_ADMIN (settlement/freeze).
Architecture
Node.js + Express + TypeScript API on PostgreSQL/Prisma. LedgerEntry is the source of truth; User.balance is a cache derived from the ledger. All balance mutations use SELECT … FOR UPDATE with monotonic per-user+currency sequences. Transactions follow a strict state machine with outbox-driven provider calls and notifications, idempotent POSTs via Idempotency-Key, unique payoutProviderRef locks, crash recovery for PENDING outbox events, and a DLQ after max retries. Admin surfaces expose kill-switches, corridor toggles, reconciliation, disputes, and immutable AdminActionLog entries. Mobile UI mockups illustrate the customer journey the API supports — they are not the primary deliverable.
Tech stack
Node.js · Express · TypeScript · PostgreSQL · Prisma · Zod · decimal.js · Jest
Key features
- Immutable ledger with DB-enforced debit/credit invariants and atomic locking
- Strict transaction state machine with compensation states for partial failures
- Exact-once internal settlement via idempotency keys, payout locks, and outbox pattern
- KYC/AML gates, velocity limits, and corridor enable/disable controls
- RBAC for SUPPORT / OPS / SUPER_ADMIN with 7-year audit retention
- Versioned /api/v1 surface: auth, send, status, receipts, compliance, and admin ops
- Observability: structured logs, metrics histograms/counters/gauges, correlation IDs
- Unit + integration test strategy (Jest / Supertest) for state machine and API flows
Engineering challenges
- Preventing double-payouts under retries, crashes, and concurrent admin actions
- Keeping ledger invariants true while User.balance remains a fast cache
- Encoding economic vs legal finality without ambiguous status APIs
- Making ops tooling safe — retries and refunds without bypassing the state machine
Tradeoffs
- Ledger-as-truth over mutable balance fields — slower writes, correct money
- Mandatory Idempotency-Key on POSTs — slightly harder clients, safer settlement
- Human-in-the-loop for sanctions/disputes over fully automated resolution
- URL versioning (/api/v1) with a 12-month deprecation policy for breaking changes
Results
- Production-oriented remittance backend with bank-grade ledger and settlement controls
- Deterministic lifecycle covering authorize → payout → refund/chargeback paths
- Ops-ready admin API: freeze, corridor kill-switches, reconciliation, DLQ replay
- Standardized machine-readable error codes for clients and compliance tooling
- Clear separation between financial source of truth (ledger) and product UX surfaces
Lessons learned
- In money systems, the ledger is the product — UI and caches are downstream
- Idempotency and outbox patterns are not optional once providers and webhooks exist
- RBAC without an immutable admin audit log is incomplete for regulated flows
- Explicit compensation states beat silent retries when payouts partially fail
Performance
Financial writes are serialized per account with row locks; arithmetic uses decimal.js with corridor-aware minor-unit rounding (2dp GBP/KES, 0dp UGX/RWF). Outbox retries use exponential backoff (interval × 2^retries, max 5) before DLQ. Auth sessions: 15m access tokens, 7d refresh. Operational SLAs include 7-day authorization expiry, 24h provisional payout window, 120-day chargeback exposure, and 24h settlement after provider confirmation.
Future improvements
- Publish the public repository and API documentation once the repo is live
- Distinct approval flow for PEPs (Politically Exposed Persons) in V2
- Deeper provider abstraction as corridors expand
- Richer dispute automation while keeping SUPER_ADMIN as the final authority