Payments · modules/billing
Adapter scaffoldStripe,
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.
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_PROVIDERrequiredSet to stripe
STRIPE_SECRET_KEYrequiredSTRIPE_WEBHOOK_SECRETrequired
Where it lives
apps/server/src/modules/billing/infrastructure/providers/stripe-payment-provider.tsapps/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.