Payments · modules/billing

Adapter scaffold

Stripe,
behind one contract.

UseDeploy models billing as a single IPaymentProvider port. Stripe is one of the adapters that plugs into it — selected by an env var, sharing the same normalized types as MercadoPago and Polar, so a provider swap is a one-file change.

What ships, and what you complete

Honesty first: the Stripe adapter is a scaffold, not a turnkey integration. The provider selection, the config validation, and the customer-portal path are wired — createCustomerPortalLink lazily imports the Stripe SDK and calls billingPortal.sessions.create. Checkout session creation and webhook parsing are typed stubs with the exact SDK calls to make written in the comments. Why scaffold and not finish it? Because the boilerplate baseline does not ship the stripe package as a dependency. You drop it in, complete the two methods against the shape that is already locked, and every consumer of the billing contract keeps working unchanged. The commitment we make now is the interface — so your code never couples to Stripe specifics.

One env var selects the provider

The composition root builds exactly one payment provider from PAYMENT_PROVIDER. Set it to stripe and supply the two secrets; the factory validates them at boot and throws a clear error if either is missing.

apps/server/.env
 1  PAYMENT_PROVIDER=stripe
 2  STRIPE_SECRET_KEY=sk_live_...
 3  STRIPE_WEBHOOK_SECRET=whsec_...
 4  
 5  # Then add the SDK the scaffold expects:
 6  #   bun add stripe   (in apps/server)

The billing contract Stripe plugs into

Customer portal — wired

createCustomerPortalLink lazy-imports the Stripe SDK and returns a hosted billing-portal URL. No SDK cost for deployments that use a different provider.

Checkout — scaffold

createCheckoutSession returns a typed "not yet implemented" until you wire stripe.checkout.sessions.create against the locked input shape.

Webhooks — scaffold

parseWebhook is stubbed with the exact call to make: stripe.webhooks.constructEvent(rawBody, sig, secret) mapped to a normalized BillingEvent.

Provider-agnostic types

Result<T, DomainError> return types are identical across Stripe, MercadoPago and Polar, so the application layer never branches on provider.

Configuration reference

Environment variables

  • PAYMENT_PROVIDERrequired

    Set to stripe

  • STRIPE_SECRET_KEYrequired
  • STRIPE_WEBHOOK_SECRETrequired

Where it lives

  • apps/server/src/modules/billing/infrastructure/providers/stripe-payment-provider.ts
  • apps/server/src/modules/billing/infrastructure/providers/index.ts

Build billing on a contract, not a vendor.

Ship with Stripe, MercadoPago or Polar behind the same typed port — and never rewrite your app to switch.