Backend architecture
The backend (trile repo) is a NestJS 11 application. This page is the engineer’s mental model.
Three process roles
Section titled “Three process roles”The same image boots into one of three roles, selected by the PROCESS_ROLE env var:
| Role | Runs | Talks to |
|---|---|---|
api | The /v1 HTTP server. Enqueues jobs, never executes them inline. | Postgres, Redis (enqueue) |
scheduler | Cron triggers: the daily billing tick, reconciliation, and idempotency-key reaper (03:00 Asia/Kathmandu). | Redis (enqueue) |
worker | Executes jobs: billing charges, webhook delivery, reconciliation. | Postgres, Redis (consume) |
Why split: a renewal or webhook delivery must never block an HTTP request, and billing must keep
running even if the API is redeployed. Queues: billing-tick, webhook-delivery,
reconciliation, idempotency-reaper, plus sms / notification-email.
Bounded contexts
Section titled “Bounded contexts”Each src/modules/* folder is a context with its own controller/service/schema and no
cross-context business-logic leaks:
wallet · billing · subscriptions · catalog (products/prices) · checkout · customers · events · webhooks · merchant · team · security · auth · payment (eSewa/Khalti/IME) · email · sms · storage · reconciliation
Correctness patterns
Section titled “Correctness patterns”These are the non-negotiables that make a payments system safe — match them in any new code.
- Money —
bigintpaisa in Postgres, hydrated to aMoneyvalue object with no.toNumber()escape hatch. See Money & paisa. - Idempotency — a global interceptor enforces
Idempotency-Keyon every mutating/v1/*request, stores the response, and replays it for 24h. See Idempotency. - Transactions —
@Transactional()(vianestjs-cls) wraps money-touching work; row locks (SELECT … FOR UPDATE) protect wallet/ledger updates. - Append-only audit & events — every money-touching state change writes an
audit_logrow; domain events go to an immutable event log surfaced at/v1/events. - Transactional outbox — webhooks are written to an outbox in the same transaction as the state change, then delivered by the worker with retries — so an event is never lost if delivery is down.
- Daily reconciliation — recomputes each wallet balance from its ledger and flags drift. See the reconciliation runbook.
- Response envelope — a global interceptor wraps results in
{ success, data, meta }; a global exception filter shapes errors. Health probes opt out via@SkipEnvelope().
Observability
Section titled “Observability”- Pino structured JSON logs with a correlation ID (
req_…) per request lifecycle; PII redacted at config time. - OpenTelemetry (
instrumentation.ts, loaded before anything else inmain.ts) + Sentry for errors; worker failures callSentry.captureException.
OpenAPI
Section titled “OpenAPI”@nestjs/swagger + nestjs-zod emit the OpenAPI document at runtime from controllers and DTOs.
It’s served at /docs-json — consumed by both frontends’ typed clients and by this docs
site’s API Reference.