FEATURES / BILLING

Stripe.
MercadoPago.
Polar.

Three providers behind one IPaymentProvider port. Switch in one line, run them side-by-side, or write your own adapter against the same interface.

Subscriptions, one-shot, customer portal.

IPaymentProvider port

Single interface: createCheckout, createPortal, getSubscription, cancelSubscription. Implementations live in infrastructure/.

Stripe

Subscriptions, one-time payments, customer portal, tax, proration, coupons. Webhook signatures verified.

MercadoPago

Suscripciones, preferencias, IPN webhooks. LATAM-friendly with proper currency and tax handling.

Polar.sh

Open-source-friendly billing with built-in license keys. Drop-in for OSS commercial tiers.

Read model

Webhooks update a Prisma read model so your app never queries Stripe live. Predictable, fast, cacheable.

Plan catalog

Plans defined in code (with Zod). Single source of truth across landing, checkout, and the dashboard.

One port, three adapters.

Your application code calls IPaymentProvider; the DI container picks the right adapter from env. Tests inject an in-memory adapter that records every call. No conditional code paths in your domain.

modules/billing/application/ports.ts
 1  export interface IPaymentProvider {
 2    createCheckout(input: CheckoutInput): Promise<Checkout>;
 3    createPortal(customerId: CustomerId): Promise<PortalUrl>;
 4    getSubscription(id: SubscriptionId): Promise<Subscription>;
 5    cancelSubscription(id: SubscriptionId): Promise<void>;
 6  }
 7  
 8  // container picks: stripe | mercadopago | polar | inMemory
 9  container.bind<IPaymentProvider>(TYPES.Payments)
10    .toFactory(() => providerFromEnv());

Charge customers in 3 currencies.

Stripe + MercadoPago + Polar shipping with one config flip.