Architecture

How PGLink is built

A modular NestJS application: controllers validate, services orchestrate, providers isolate each gateway.

Layers

┌───────────────┐   ┌───────────────┐   ┌─────────────────────┐
│ Controllers   │──▶│ Services      │──▶│ Models / MongoDB    │
│ (HTTP + DTOs) │   │ (business)    │   │ (Mongoose)          │
└───────────────┘   └──────┬────────┘   └─────────────────────┘
       ▲                   │
       │                   ▼
┌───────────────┐   ┌─────────────────────────────────────────┐
│ Guards/Pipes  │   │ Providers: Xendit · Midtrans · Mayar.id │
│ JWT/API-Key   │   └─────────────────────────────────────────┘
└───────────────┘

Modules

Provider abstraction

Every gateway implements PaymentProvider (and SubscriptionProvider for recurring billing). PaymentsService.getProvider() maps the request's provider enum to an implementation, so adding Mayar.id required no change to the request contract.

enum PaymentProvider {
  XENDIT = 'XENDIT',
  MIDTRANS = 'MIDTRANS',
  MAYAR = 'MAYAR',
}

Payment flow

  1. POST /payments with an X-API-Key.
  2. ApiKeyGuard resolves the application.
  3. PaymentsService selects the provider and calls createPayment.
  4. The provider calls the gateway and returns an externalId + checkoutUrl.
  5. The payment is stored with status PENDING.
  6. On callback, the status is reconciled and forwarded to the application's webhook.

Data model

MongoDB collections defined as Mongoose schemas. Every document uses an ObjectId _id, serialized to the API as id.

CollectionKey fields
applications_id, name, apiKey, webhookUrl, subscriptionWebhookUrl, isActive, createdAt, updatedAt
payments_id, applicationId (ref), externalId, amount, status, provider, paymentMethod, checkoutUrl, metadata, processedAt, createdAt, updatedAt
subscriptions_id, applicationId (ref), externalSubscriptionId, amount, status, provider, billingCycle, nextBillingDate, createdAt, updatedAt

Security