Skip to content

yusupscopes/payrouter

Repository files navigation

Payment Application Gateway

One Gateway. Every Payment Provider.

Node.js TypeScript Hono PostgreSQL Drizzle ORM Redis

License: MIT Tests CI Coverage


What is this?

A centralized Payment Gateway that abstracts multiple payment providers behind a single, unified REST API. Instead of every internal service importing Stripe, Midtrans, or Xendit SDKs, they call one API. The gateway handles provider selection, request translation, retry logic, error normalization, and audit logging.

Design principle: "Internal services should never import a payment SDK. They should never know whether a charge went through Stripe or Midtrans. That's the gateway's problem — not theirs."


At a Glance

Metric Result
Providers unified 3 (Stripe, Midtrans, Xendit)
API surface for all operations 1
Operations supported charge · refund · verify · query · list
Observability Correlation IDs, Prometheus metrics, health checks, structured JSON logging
Async webhooks BullMQ queue with exponential backoff + deduplication
Provider logic in consumers 0
Graceful shutdown SIGTERM/SIGINT, LB drain, resource cleanup
Request logging Structured JSON with hashed API keys, per-route body limits
Time to add a new provider ~1 day

Why This Exists

The Problem

  • Duplicated integration logic — Every team re-implements provider auth, request signing, and error parsing
  • Inconsistent error handling — Midtrans returns status_code: "406" as strings, Stripe throws typed exceptions, Xendit uses HTTP codes
  • Adding providers touches everything — No single place to add a new regional gateway
  • No audit trail — Payment interactions logged inconsistently across services

The Solution

Before (Direct Integration) After (Via Gateway)
SDK imports Each service imports Stripe directly One REST API for all ops
Error handling Codes differ per provider Normalized error shape everywhere
Retry logic Duplicated everywhere Lives in one place
New provider Update N services Write one adapter
Audit trail Inconsistent or missing Every transaction logged centrally

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                      PAYMENT GATEWAY SERVICE                         │
└─────────────────────────────────────────────────────────────────────┘

Billing Service  ─────┐
Order Service    ─────┤──► POST /v1/payments/charge
Subscription Svc ─────┘    POST /v1/payments/refund
                           POST /v1/payments/verify
                           GET  /health  ← provider connectivity
                           GET  /metrics ← Prometheus metrics
                                      │
                            ┌──────────▼──────────┐
                            │    Gateway Core      │
                            │  ┌─────────────────┐ │
                            │  │ Provider Router │ │  ← selects adapter
                            │  │ Retry Manager   │ │  ← 3 attempts + jitter
                            │  │ Circuit Breaker │ │  ← opens after 5 failures
                            │  │ Error Normalizer│ │  ← unified error shape
                            │  │ Audit Logger    │ │  ← every tx logged
                            │  │ Metrics         │ │  ← latency, errors, retries
                            │  └─────────────────┘ │
                            └──────────┬────────────┘
                                       │
                ┌─────────────────────┼─────────────────────┐
                ▼                     ▼                     ▼
       StripeAdapter         MidtransAdapter         XenditAdapter
    (implements IPaymentProvider)
                │                     │                     │
          Stripe API           Midtrans API           Xendit API

Async Webhook Pipeline (BullMQ + Redis)
  POST /v1/webhooks/:provider → verify → enqueue → worker processes

Core Design Principles

  • Open/Closed Principle — Adding a provider = one new adapter. Zero changes to core or consumers.
  • Dependency Inversion — Core references IPaymentProvider, never concrete adapters.
  • Single Responsibility — Each adapter knows only its provider's quirks.
  • Encapsulation — Provider auth, webhooks, and error codes are invisible to consumers.
  • Observability — Every request gets a correlation ID. Every operation emits Prometheus metrics.
  • Resilience — Circuit breaker prevents cascade failures. Jittered backoff avoids thundering herd.

Tech Stack

Layer Technology
Runtime Node.js 22+
Language TypeScript 5.6+ (strict mode)
Web Framework Hono
Validation Zod
Database PostgreSQL 16+
ORM Drizzle ORM
Queue BullMQ + Redis
Metrics Prometheus (prom-client)
Testing Jest + ts-jest
Monorepo pnpm workspaces + Turborepo
Linting Biome
CI/CD GitHub Actions

Provider SDKs

  • Stripestripe (Payment Intents, Refunds, Webhook HMAC verification)
  • Midtransmidtrans-client (Core API, Snap, notification handling)
  • Xenditxendit-node (Invoices, Refunds, callback token verification)

Quick Start

Prerequisites

  • Node.js 22+
  • pnpm 10+
  • PostgreSQL 16+ (or Docker)
  • Redis 7+ (or Docker)

1. Clone & Install

git clone https://github.com/yourusername/payment-application-gateway.git
cd payment-application-gateway
pnpm install

2. Configure Environment

cp apps/server/.env.example apps/server/.env

Edit apps/server/.env:

# Database
DATABASE_URL=postgresql://postgres:password@localhost:5432/payment_gateway
DATABASE_URL_TEST=postgresql://postgres:password@localhost:5432/payment_gateway_test

# Redis (required for rate limiting, queue, and replicated deployments)
REDIS_URL=redis://localhost:6379

# Provider credentials
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
MIDTRANS_SERVER_KEY=SB-Mid-server-...
XENDIT_SECRET_KEY=xnd_test_...

# API Key authentication (comma-separated)
API_KEYS=api-key-1,api-key-2

# Logging
LOG_LEVEL=info
LOG_WEBHOOK_BODIES=false

# Operational Readiness
SHUTDOWN_TIMEOUT_MS=10000
WEBHOOK_DEDUP_TTL_HOURS=72

# Server
CORS_ORIGIN=http://localhost:3001
NODE_ENV=development

3. Start Infrastructure

# Using Docker Compose (PostgreSQL + Redis)
pnpm docker:up

# Or start services individually
pnpm db:start
pnpm redis:start

4. Push Schema

pnpm db:push

5. Run Development Server

pnpm dev:server

The API is running at http://localhost:3000.


API Reference

Payment Operations

Charge

POST /v1/payments/charge
{
  "provider": "stripe",
  "amount": 1000,
  "currency": "USD",
  "paymentMethod": "pm_card_visa",
  "description": "Order #12345",
  "metadata": { "orderId": "12345" },
  "customerId": "cus_xxx"
}

Response:

{
  "success": true,
  "transactionId": "txn_abc123...",
  "amount": 1000,
  "currency": "USD",
  "provider": "stripe",
  "providerRef": "pi_xxx",
  "raw": { /* full provider response */ }
}

Refund

POST /v1/payments/refund
{
  "provider": "stripe",
  "transactionId": "txn_abc123...",
  "amount": 1000,
  "reason": "requested_by_customer"
}

Verify

POST /v1/payments/verify
{
  "provider": "stripe",
  "transactionId": "pi_xxx"
}

Webhooks

POST /v1/webhooks/:provider

Providers accept webhooks at:

  • /v1/webhooks/stripe — verifies stripe-signature header
  • /v1/webhooks/midtrans — verifies notification signature
  • /v1/webhooks/xendit — verifies x-callback-token header

Async Processing: When Redis is available, webhooks are verified synchronously and then enqueued for async processing via BullMQ (returns 202 Accepted with queued: true). When Redis is unavailable, they fall back to synchronous processing (200 OK).

Health

GET /health

Returns per-provider connectivity status. 200 if all providers are healthy, 503 if any are degraded.

Metrics

GET /metrics

Prometheus exposition endpoint with operation latency, error rate, and retry counters.


Provider Configuration

Each provider is configured via environment variables and registered at runtime.

// Adapters are registered in src/app.ts
registry.register(new StripeAdapter({ secretKey: env.STRIPE_SECRET_KEY }));
registry.register(new MidtransAdapter({ serverKey: env.MIDTRANS_SERVER_KEY }));
registry.register(new XenditAdapter({ secretKey: env.XENDIT_SECRET_KEY }));

Adding a New Provider

  1. Create a new adapter in src/adapters/<provider>/
  2. Implement IPaymentProvider interface
  3. Register in src/app.ts
  4. Add tests in src/adapters/<provider>/<provider>-adapter.test.ts

That's it. Zero changes to routes, core, or consumers.


Security

  • API Key Authentication — All payment endpoints require x-api-key header against a configured allowlist
  • Per-Key Rate Limiting — Redis-backed rate limiting restricts each API key to 100 requests/minute
  • Webhook Signature Verification — Stripe (HMAC-SHA256), Midtrans (SHA512), and Xendit (callback token) signatures are verified before accepting events
  • Circuit Breaker — Prevents cascade failures by opening after repeated provider errors
  • Audit Logging — Every transaction is recorded in PostgreSQL with correlation IDs for traceability

Error Handling

All errors are normalized to a unified shape:

{
  "error": {
    "code": "CARD_DECLINED",
    "message": "The card was declined",
    "retryable": false
  }
}

Error Codes

Code Description Retryable
INSUFFICIENT_FUNDS Card has insufficient balance No
CARD_DECLINED Card was declined by issuer No
EXPIRED_CARD Card has expired No
RATE_LIMITED Too many requests Yes
GATEWAY_ERROR Provider API error Yes
INVALID_REQUEST Bad request data No
UNAUTHORIZED Authentication failed No
NOT_FOUND Provider not registered No

Testing

# Run all tests
pnpm test

# Run with coverage
pnpm test -- --coverage

# Watch mode
pnpm test:watch

# Run specific test file
pnpm test -- --testPathPatterns="stripe-adapter"

Test Structure

  • Unit tests — Colocated with source (*.test.ts)
  • Integration teststests/integration/
  • Unit teststests/unit/
  • Contract tests — Adapter interface verification

Coverage (as of latest run)

Suite Coverage
Overall 90%
Core (registry, retry, audit, circuit breaker, metrics, logger) 99%
Adapters 77-94%
Routes 87%
Middleware 90%
Queue 87%

Project Structure

payment-application-gateway/
├── apps/
│   └── server/
│       ├── src/
│       │   ├── adapters/          # Provider adapters
│       │   │   ├── stripe/
│       │   │   ├── midtrans/
│       │   │   └── xendit/
│       │   ├── core/              # Gateway core
│       │   │   ├── provider-registry.ts
│       │   │   ├── payment-gateway.ts
│       │   │   ├── retry-manager.ts
│       │   │   ├── circuit-breaker.ts
│       │   │   ├── audit-logger.ts
│       │   │   ├── transaction-store.ts   # NEW — query abstraction
│       │   │   ├── metrics.ts
│       │   │   ├── logger.ts           # NEW — pino structured logger
│       │   │   └── request-context.ts
│       │   ├── routes/            # API routes
│       │   │   ├── payments.ts
│       │   │   ├── transactions.ts    # NEW — query endpoints
│       │   │   ├── providers.ts       # NEW — provider introspection
│       │   │   ├── webhooks.ts
│       │   │   ├── webhook-events.ts  # NEW — async event history
│       │   │   ├── health.ts
│       │   │   ├── metrics.ts
│       │   │   └── docs.ts            # NEW — OpenAPI spec + Scalar UI
│       │   ├── middleware/        # Middleware
│       │   │   ├── error-handler.ts
│       │   │   ├── api-key-auth.ts
│       │   │   ├── rate-limiter.ts
│       │   │   ├── correlation-id.ts
│       │   │   ├── request-logger.ts     # NEW — structured HTTP logging
│       │   │   ├── body-size-limit.ts    # NEW — per-route body limits
│       │   │   └── pagination.ts         # NEW — pagination helper
│       │   ├── queue/             # BullMQ queue + worker
│       │   │   ├── webhook-queue.ts
│       │   │   └── webhook-worker.ts
│       │   ├── types/             # Shared types
│       │   │   ├── api.ts             # NEW — API response types
│       │   │   └── payment.ts
│       │   ├── app.ts             # App factory
│       │   └── index.ts           # Server bootstrap
│       └── tests/
│           ├── __mocks__/         # Test mocks
│           ├── integration/       # Integration tests
│           └── unit/              # Unit tests
├── packages/
│   ├── db/                        # Database schema & client
│   ├── env/                       # Environment validation
│   ├── redis/                     # Redis client
│   └── config/                    # Shared TS config
├── docs/
│   ├── use-case.md               # Case study
│   └── specs/                    # Implementation specs
└── package.json                   # Workspace config

Scripts

# Development
pnpm dev:server          # Start server in watch mode
pnpm docker:up           # Start PostgreSQL + Redis via Docker Compose
pnpm db:start            # Start PostgreSQL via Docker
pnpm redis:start         # Start Redis via Docker
pnpm db:push             # Push schema to database (dev only — use migrations for production)
pnpm db:generate         # Generate a versioned migration from schema changes
pnpm db:migrate          # Apply pending migrations
pnpm db:studio           # Open Drizzle Studio

# Quality
pnpm check-types         # Type-check all packages
pnpm check               # Run Biome lint + format
pnpm test                # Run all tests
pnpm test -- --coverage  # Run tests with coverage report

# Build
pnpm build               # Build all packages and apps

Design Decisions

1. Custom Retry Manager (not p-retry)

The retry logic is domain-specific: retryable vs non-retryable is a business decision, not infrastructure. Adapters classify errors. The retry manager just executes with jittered exponential backoff.

2. Circuit Breaker

Provider failures are isolated. After 5 failures in 60 seconds, the circuit opens for 30 seconds, returning fast failures instead of slow timeouts.

3. Idempotency in Gateway Core

Callers don't pass idempotency keys. The gateway generates txn_ prefixed IDs automatically. Prevents duplicate charges under retry.

4. Raw Response Storage

Every transaction stores the raw provider response alongside the normalized result. When providers change schemas, historical data is preserved.

5. Runtime Provider Resolution

The ProviderRegistry uses a Map<ProviderName, IPaymentProvider>. No compile-time imports of adapter classes in the gateway core.

6. AsyncLocalStorage for Correlation IDs

Instead of passing a Hono Context through every function signature, we use Node.js AsyncLocalStorage to propagate correlation IDs into core logic, adapters, and background workers transparently.

7. Graceful Degradation

Redis is required for full functionality (rate limiting, async webhooks), but the gateway starts and serves requests without it. Missing features log warnings rather than crashing.


Roadmap

Completed

  • Add Docker Compose for one-command setup
  • Implement webhook event queue (Redis/BullMQ)
  • Add health check endpoint with provider status
  • Add metrics and monitoring (Prometheus)
  • Implement API key authentication and rate limiting (Redis)
  • Add correlation ID propagation for observability
  • Circuit breaker for provider resilience
  • Jittered exponential backoff in retry manager

Next Up

Note: For a payment gateway, reliability is security. A bad deploy or duplicate charge is a financial incident. This roadmap is reordered by actual production risk, not by feature category.

P0 — Do Before Production (Real Money at Risk)

  • GitHub Actions CI pipeline — type check, lint, test, build on every PR. Prevents deploying broken code to a financial system.
  • Implement idempotency key caching (Redis) — cache txn_ IDs in Redis to short-circuit duplicate charges under retries or network blips.
  • Secrets management — migrate from flat .env files to a secrets manager (AWS Secrets Manager, HashiCorp Vault). Prevents credential leaks.

P1 — High Value, Do Soon After P0

  • Smoke tests after deployment — verify /health and sample charge/refund in staging before promoting to production. Catches config drift and credential issues.
  • Database migration verification in CI — validate migrations run cleanly and don't corrupt transaction history.
  • Admin/ops endpoints/admin/queue-status, retry failed jobs, drain queue. Operational visibility without SSH.

P2 — Defense in Depth (Important but Not Blocking)

  • Webhook IP allowlisting — restrict webhook endpoints to known provider IP ranges. Complements signature verification.
  • Docker image build and push to registry (GHCR or ECR). Required for deployment; the CI pipeline is the prerequisite.
  • Staging deployment pipeline — auto-deploy main branch to staging environment.
  • Production deployment pipeline — tagged releases with rollback capability.

Security & Reliability — Missing from Original Plan

  • Input validation / sanitization hardening — strict bounds checking on all webhook payloads before queueing.
  • Sensitive data logging audit — verify raw card numbers or PCI-relevant fields never hit structured logs.
  • Database encryption at rest — encrypt transaction data (customer IDs, amounts, raw responses).
  • Endpoint-specific rate limiting — differentiate webhook (higher) vs. charge/refund (strict) limits.

Strategic & Expansion

  • Support for more providers (PayPal, Braintree, etc.)
  • Transaction reconciliation job — periodic worker comparing gateway vs. provider state.
  • Load testing / performance benchmarks — documented and run in CI.
  • Multi-environment deployment config — K8s manifests or Terraform.
  • Redis Sentinel / Cluster support — failover for replicated deployments.
  • Add Bull Dashboard for queue monitoring.
  • Infrastructure as Code — Terraform or Pulumi for staging and production environments.

Contributing

Contributions are welcome! Please read the following before submitting PRs:

  1. Follow the existing code style (Biome handles most of this)
  2. Write tests for new features
  3. Update the spec if you change architecture
  4. Ensure pnpm check-types and pnpm test pass locally
  5. CI runs automatically on every PR — lint, type check, unit tests, integration tests (with PostgreSQL + Redis), migration verification, and build must all pass before merge
# Before committing
pnpm check        # Format & lint
pnpm check-types  # Type check
pnpm test         # Run tests

License

MIT © Yusup


Built with ❤️ for teams tired of writing payment integrations.

About

Unified payment orchestration gateway that routes transactions across multiple payment providers through a single API.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages