Skip to content
All posts

Your payment provider is a dependency, not a decision

Why we built provider switching into the billing layer, and what it takes to move paying customers without dropping a single renewal.

Ada Whitfield

3 min read

Every SaaS billing tutorial ends the same way: you pick a provider, wire up a webhook, and ship. Nobody writes the next chapter, which is the week your account gets flagged, or the provider exits your market, or their pricing changes and the maths stops working.

We wanted that chapter to be boring. So the billing layer here has no single provider in it.

The contract, not the SDK

The whole system talks to one interface. A provider implements checkout, a customer portal, plan changes, quantity updates, usage reporting, and a webhook handler. It also declares what it cannot do:

capabilities: {
  customerPortal: true,
  changePlan: true,
  reportUsage: false,
  promoCodes: true,
  trials: true,
}

That last part matters more than it looks. Providers differ, and pretending otherwise is how you end up with a button that throws in production. When a capability is missing, the UI does not render the control at all, and the server function returns a typed outcome saying why rather than a generic failure.

Switching without a migration weekend

The naive version of "switch providers" is a big-bang migration: export customers, import them, hope the subscription ids line up. That means downtime, and it means a window where a renewal can fire against a provider you have already stopped listening to.

Instead the switch is a boundary in time. Change the active provider, and:

  • New checkouts go to the new provider immediately.
  • Existing subscriptions keep billing where they are until their current period ends.
  • Webhooks stay live for both, because the old provider is still the source of truth for anyone it is still charging.
  • Renewals migrate one at a time, each customer moving at their own period boundary.

Nobody is double-billed, nobody loses access, and there is no weekend where the answer to "am I subscribed?" is "it depends which system you ask".

What we learned building it

Store the provider on the subscription, not the workspace. A workspace is not billed by one provider; a subscription is. Once we moved the field, the migration logic collapsed from a special case into an ordinary query.

Preserve internal metadata across webhook upserts. An early version overwrote the whole metadata object on every incoming event, which quietly reset the flags that stop dunning emails from re-sending. Now anything prefixed with a double underscore survives the upsert.

Fall back to the old provider for the portal button. When a customer is past due and mid-migration, the "manage billing" link has to point at whoever is actually holding their card. Resolving the provider from the subscription rather than from configuration fixed a support ticket we had not received yet.

Is this over-engineering?

It is one interface and three implementations, and it took less time than the Stripe integration alone. The alternative is a codebase where the provider's name appears in two hundred places, which is not a design so much as a bet that nothing will ever change.

Provider risk is real. Build for it once, early, while the billing code is small enough to change.

Ada Whitfield

3 min read

Subscribe

Keep reading