saascode

One payment layer, 18 processors, and no fake parity

A self-hosted SaaS payment layer with Stripe, PayPal, 16 more processors, shared checkout and webhooks, plus honest provider-by-provider limits.

fabric · aug 21, 2026 · 4 min read

A self-hosted SaaS with Stripe and PayPal should not need two billing architectures. It needs one payment contract, provider-specific adapters, and an honest answer when a processor cannot offer the same feature as another. The included payment layer does exactly that across 18 processors.

The current adapter set is measured from the implementation, not a marketing list: Stripe, PayPal, Paddle, Mollie, Mercado Pago, dLocal, PagSeguro, Razorpay, Paystack, Flutterwave, Coinbase Commerce, NOWPayments, Lemon Squeezy, Alipay, WeChat Pay, Komoju, Opn, and KakaoPay.

That mix is deliberately broader than the usual US-and-Europe default. It includes global processors, merchant-of-record options, Latin American rails, African processors, Asian wallets, and cryptocurrency checkout. A product can start with the provider that fits its first market without throwing away the billing model when it expands.

One contract, different capabilities

Every adapter implements the same payment-provider interface. The contract covers customer creation, subscriptions, cancellation, checkout sessions, webhook handling, and customer-portal URLs. It also exposes capability flags for product creation, subscriptions, one-time payments, and portal support.

Those flags are the important part. The code does not pretend all 18 processors are interchangeable. All 18 declare one-time payment support. Fifteen declare subscription support. Seven can create remote products and prices through their API. Four expose a customer billing portal. A route checks portal support before trying to redirect the customer.

That produces a cleaner application boundary. Product code can ask for a checkout session through one interface. Admin code can ask whether the selected processor can create a price. The user experience can hide or explain a portal action when the provider does not have one. Differences remain visible as data instead of surfacing later as runtime surprises.

Provider selection is an organization setting

The payment factory resolves the active provider for an organization. It then loads the credentials and non-secret settings that adapter needs. The credential resolver checks an organization-specific value first and can fall back to the platform organization, so a product can use one platform account or allow a tenant-level override without creating a second checkout stack.

No credential means no provider instance. The factory returns null rather than creating a half-configured adapter. Checkout routes can turn that into a clear configuration error instead of sending a customer into a broken payment flow.

The full provider list is also available as a configuration scan. The factory walks the 18 supported slugs and returns only adapters whose required credentials are present. That is what makes a multi-provider pricing screen possible: the product can distinguish “supported in code” from “configured for this organization.”

Webhooks become product events

Payments are not finished at checkout. The shared webhook route accepts a provider slug and sends the request through a common handler. Each adapter verifies and parses its native payload. Stripe keeps a separate typed translator; the other adapters pass through a universal translator that maps provider event names into a common event vocabulary.

The map covers events such as completed or failed payments, subscription creation and updates, cancellation, past-due state, and refunds where the provider reports them. Unrecognized events return null and are logged as unhandled instead of being forced into the wrong business event.

Signature extraction is provider-aware. Stripe, Paddle, Razorpay, Paystack, Coinbase Commerce, dLocal, Lemon Squeezy, WeChat Pay, Komoju, and others each use their expected header. PayPal passes the full group of verification headers. A few processors use a different verification model: Mollie re-fetches, Alipay reads the signature from the payload, and KakaoPay relies on its redirect flow.

After translation, the event registry updates shared billing state. That gives the product one place to react to a subscription or payment regardless of which external event name triggered it.

What the buyer operates

Plans remain independent from processors. Provider-specific product and price IDs live in a separate table with synchronization status and an error field. The admin can see which external prices exist for each internal plan. Checkout only offers providers with a synchronized or manually supplied price ID.

The normal path is straightforward: configure credentials, choose the active provider, create or attach external prices, and expose checkout. A product can configure more than one processor at the same time while retaining one active default.

This is useful for regional expansion. A product does not need to rename its internal plans or fork its entitlement logic to add Mercado Pago, Razorpay, or Paystack. It adds the provider credentials and external price mapping that market needs.

The honest limit

Eighteen adapters do not mean eighteen identical experiences. Eleven cannot create remote products and prices through the included API. Three do not declare subscription support. Four have a customer portal. That is why the capability flags exist.

Webhook behavior also follows each provider's real model. An adapter can only translate events the processor sends and the map recognizes. An unrecognized native event is ignored deliberately. A product that depends on a provider-specific dispute, payout, or tax event needs to add that event to the adapter and the product's own handler.

The layer does not supply merchant accounts, approve a business with a processor, or configure external dashboards. Credentials, webhook endpoints, regional eligibility, and remote product setup still belong to the account owner. “Supported” means the application adapter is present. It does not mean the external provider has been activated for the buyer.

Why every product gets the abstraction

Even a product launching with Stripe benefits from the boundary. Checkout code talks to a payment contract rather than importing one provider across the application. Webhook effects become common events. Plans keep their own identity.

That makes the second provider a configuration and adapter problem instead of a billing rewrite. It also makes the first provider more honest: unsupported portal or subscription behavior is declared, not discovered after launch.

The result is not artificial parity. It is a consistent application layer with measured differences underneath. That is the useful version of multi-provider payments.

end
Self-hosted SaaS payments with Stripe and PayPal