Core banking / e-wallet backend for Nexa — a digital social media and e-wallet platform. Built with NestJS, PostgreSQL (via Prisma), and Redis.
MVP scope. This scaffold implements the minimum viable set of endpoints needed to register a user, verify identity (KYC), hold a wallet balance, and move money (cash-in, cash-out, P2P transfer) — with the security and compliance scaffolding a regulated e-money issuer in the Philippines needs in place from day one. It is a strong foundation, not a finished, audited banking system — see "Before going live" below.
- Framework: NestJS 10 (Express platform)
- Database: PostgreSQL via Prisma ORM
- Cache/Sessions: Redis
- Auth: JWT (short-lived access + rotating refresh tokens), Argon2id password hashing, optional TOTP MFA
- Validation: class-validator / class-transformer, whitelist-strict DTOs
- Docs: Swagger/OpenAPI (auto-disabled in production)
src/
auth/ registration, login, refresh/rotation, MFA
users/ profile, KYC submission & history
wallets/ balance, ledger/statement
transactions/ cash-in, cash-out, P2P transfer (core money-movement engine)
virtual-accounts/ partner-bank virtual account issuance + inbound credit webhook
instapay/ InstaPay participating-bank directory + instant interbank transfer
loans/ loan products, application/auto-decisioning, amortized repayment
audit/ append-only compliance audit trail
health/ liveness/readiness probes
common/ guards, filters, interceptors, decorators, encryption service
config/ typed configuration + env validation
prisma/ Prisma service/module
stellar/ Stellar wallet provisioning and asset-trustline support
prisma/
schema.prisma data model
seed.ts seeds tier limits, InstaPay bank directory, starter loan products
cp .env.example .env
npm install
npm run prisma:migrate
npm run seed
npm run start:devAdd these values to your .env before running the API:
STELLAR_NETWORK=testnet
STELLAR_HORIZON_URL="https://horizon-testnet.stellar.org"
STELLAR_DISTRIBUTOR_SECRET="replace-with-your-distributor-secret"
STELLAR_STARTING_BALANCE_XLM=5
STELLAR_AUTO_FUND=true
STELLAR_ISSUER_PHP_PUBLIC="replace-with-php-issuer-public-key"
STELLAR_ISSUER_USD_PUBLIC="replace-with-usd-issuer-public-key"
STELLAR_ISSUER_EUR_PUBLIC="replace-with-eur-issuer-public-key"
STELLAR_ISSUER_INR_PUBLIC="replace-with-inr-issuer-public-key"
STELLAR_ISSUER_GBP_PUBLIC="replace-with-gbp-issuer-public-key"
STELLAR_ISSUER_JPY_PUBLIC="replace-with-jpy-issuer-public-key"When STELLAR_AUTO_FUND=false or STELLAR_STARTING_BALANCE_XLM=0, the API will provision a Stellar keypair for the wallet without creating the on-chain account. The user must deposit XLM to activate the wallet.
NETBANK_CLIENT_ID="replace-with-your-netbank-client-id"
NETBANK_CLIENT_SECRET="replace-with-your-netbank-client-secret"
PARTNER_WEBHOOK_SECRET="replace-with-a-long-random-secret-used-to-verify-inbound-webhooks"
NETBANK_VCA_PARTNER_ALIAS="12345"
INSTAPAY_CEILING_PHP=50000PARTNER_WEBHOOK_SECRET is used to authenticate inbound virtual-account credit notifications from the partner banking integration.
The API exposes new InstaPay QR endpoints:
POST /instapay/qr— issue a payment-ready InstaPay QR payload via Netbank.POST /instapay/qr/decode— decode/validate a scanned InstaPay QR payload via Netbank.
See docs/instapay-qr.md for request/response examples.
Beyond the core wallet/transfer engine, this API implements three additional capabilities modeled on the embedded-banking (BaaS) feature set that Philippine BaaS providers like NetBank.ph publicly describe offering to their fintech partners — account issuance, payments, and loan support, all via API. This is inspiration for scope, not a clone of their actual implementation: NetBank.ph's real API contracts, partner integrations, and underwriting logic are proprietary and not publicly documented, so nothing here reproduces their actual system — it's an independent implementation built to cover the same category of features for Nexa's own wallet.
- Virtual accounts (
/virtual-accounts) — issue a partner-bank virtual account number mapped 1:1 to a user's wallet, so any ordinary bank transfer into that number becomes a wallet cash-in. Includes an HMAC-signed inbound webhook (POST /virtual-accounts/webhook/credit) for the partner bank to notify us when funds land — idempotent on the partner's own reference so retried webhook deliveries never double-credit the wallet. - InstaPay transfers (
/instapay) — send funds instantly to any InstaPay-participating bank or e-wallet. Validates the destination against a participating-bank directory and enforces InstaPay's own per-transaction ceiling (configurable, seeINSTAPAY_CEILING_PHP) in addition to the account's KYC-tier limits. - InstaPay QR support — new
POST /instapay/qrandPOST /instapay/qr/decodeendpoints let the API issue payment-ready QR payloads and decode/validate scanned payloads through Netbank. - Stellar wallet assets — new user registrations now provision a Stellar
wallet for on-chain asset handling and trustline management across PHP, USD,
EUR, INR, GBP, and JPY. The integration is wired through
StellarServiceand uses issuer public keys from environment configuration.
- Transport & headers: Helmet (HSTS, no
X-Powered-By), compression, strict CORS allowlist,trust proxyfor correct client IPs behind a load balancer. - AuthN/AuthZ: Argon2id password hashing, JWT access (short TTL) + refresh token rotation with reuse detection, account lockout after repeated failed logins, optional TOTP-based MFA, global
JwtAuthGuard(routes are private by default; opt out with@Public()). - Input hardening: global
ValidationPipewithwhitelist+forbidNonWhitelisted— unexpected/extra fields are rejected outright, not silently dropped. - Rate limiting: global default throttle plus tighter per-route throttles on auth and money-movement endpoints.
- PII protection: AES-256-GCM field-level encryption for government ID numbers and MFA secrets; one-way hashing for values only ever compared (device fingerprints, refresh tokens at rest).
- Error handling: a global exception filter ensures stack traces, SQL fragments, and internal paths are never returned to clients.
- Idempotency: all money-movement endpoints require a client-supplied
Idempotency-Keyheader, preventing duplicate processing on retries/timeouts. - Financial integrity: an append-only, double-entry ledger (
LedgerEntry) backs every wallet balance change; balance mutations use optimistic locking (Wallet.version) inside serializable DB transactions with automatic retry on conflict. - Non-root containers: the production Docker image runs as an unprivileged user with a healthcheck and
dumb-initfor clean signal handling.
This scaffold is built with BSP Circular 1108 (E-Money Issuers) and the Anti-Money Laundering Act (AMLA) in mind. It is not a compliance certification — Nexa's compliance officer and counsel should review before go-live — but the following are wired in as a starting point:
| Requirement | Where it lives |
|---|---|
| Tiered KYC with balance/transaction ceilings | KycTier enum, TierLimit table, KycTierGuard, limit checks in TransactionsService |
| Minimum age (18+) for account opening | AuthService.register() |
| Covered Transaction Report threshold (≥ PHP 500,000) | Transaction.requiresCtr, compliance.ctrThresholdPhp config |
| AML monitoring flag for high-value transfers | Transaction.amlFlagged / HELD_FOR_REVIEW status |
| Immutable, append-only financial ledger | LedgerEntry model (never updated/deleted) |
| Full audit trail (who/when/where) for security & financial events | AuditLog model + AuditService, surfaced to users via /users/me self-service (Data Privacy Act right-to-access) |
| Encryption of sensitive personal/financial data at rest | EncryptionService (AES-256-GCM) applied to ID numbers, MFA secrets |
| Long-tail record retention (BSP requires multi-year retention) | soft-delete (deletedAt) rather than hard delete; compliance.auditLogRetentionDays config |
| InstaPay per-transaction ceiling (PPMI/BSP-set, revised periodically) | compliance.instapayCeilingPhp config, enforced in TransactionsService.instapayTransfer() |
| Authenticity of inbound partner-bank fund notifications | PartnerWebhookSignatureGuard (HMAC-SHA256, fails closed if unconfigured) on the virtual-account credit webhook |
- Route
HELD_FOR_REVIEWtransactions to a real compliance/AML case-management workflow (this scaffold marks them but does not implement a review queue/UI). - Wire actual Suspicious Transaction Report (STR) and Covered Transaction Report (CTR) submission to the AMLC.
- Add a formal admin/compliance-officer role and permission model (this MVP only has end-user auth).
- Get an external security assessment / penetration test before handling real funds.
- Confirm data residency, encryption-at-rest for the database itself, and backup/DR procedures meet BSP technology risk management guidelines.
npm test # unit tests
npm run test:e2e # end-to-end tests (spins up the full Nest app)
npm run test:cov # coverage reportProprietary — Techyra Private Ltd / Nexa. All rights reserved.