0%
💸 Launch Offer for the first 50 founders: Save at least $132.
★ FeaturedPayments & Production#Stripe#Next.js#Payments

Stop Hardcoding Stripe! How to Build a Universal Payment Adapter for Next.js?

Alap Putatunda

Written by AI &

Alap Putatunda

June 7, 2026
A visual list numbering 1 to 5, displaying logos for five payment providers: Stripe, Polar, Creem, Lemon Squeezy, and Dodo Payments.
A visual list numbering 1 to 5, displaying logos for five payment providers: Stripe, Polar, Creem, Lemon Squeezy, and Dodo Payments.

TLDR: In 2026, as SaaS monetization models shift rapidly between subscriptions and usage-based billing, hardcoding a payment provider into dozens of files creates a refactor bill that runs into weeks when you need to switch. The adapter pattern centralizes all provider logic behind one interface, so swapping Stripe for Lemon Squeezy means changing one env var — not 40 files. Vibe44 ships this abstraction pre-built, covering Stripe, Lemon Squeezy, Polar, Dodo, and Creem with normalized webhooks across all five.

You wired Stripe into 40 files. Now you need a merchant-of-record for global VAT — and you're staring down a refactor that touches every checkout call. Every webhook handler. Every subscription status check. That's not a refactor; that's a rewrite. And it's entirely avoidable.

The adapter pattern is the fix. Here's how it works, why it matters for payment infrastructure specifically, and how Vibe44 ships it ready to go.

The hidden cost of payment-provider lock-in — webhooks, types, and pricing handcuffs

The obvious cost of provider lock-in is the checkout call. You have stripe.checkout.sessions.create() scattered across your codebase, and swapping that out is annoying but mechanical.

The real cost is everything else.

Webhooks are the worst offender. Stripe sends a customer.subscription.updated event with a specific shape. Lemon Squeezy sends subscription_updated with a completely different payload structure. Polar has its own format entirely. If you've written webhook handlers that destructure provider-specific payloads — and you have, because that's the natural thing to do — then every handler is now coupled to a provider.

Type definitions compound the problem. When you import Stripe.Subscription directly into your business logic, that type becomes a dependency. Switching providers means hunting down every type reference and replacing it with the new provider's equivalent.

Then there's pricing. Stripe's fee structure differs from Lemon Squeezy's merchant-of-record model, which differs from Polar's approach to open-source monetization. Each provider has distinct strengths: Stripe gives you maximum control and global reach; Lemon Squeezy handles VAT and tax compliance as a merchant of record so you don't have to register in every jurisdiction; Polar is purpose-built for developer and open-source products with usage-based billing. Your business needs change. What works for a US-only launch may not work when you're selling to European customers and suddenly need VAT compliance.

If your codebase treats the payment provider as infrastructure — not as a swappable dependency — you pay the full refactor cost every time your needs shift.

The adapter pattern for payments — one interface, swappable implementations, normalized webhooks

The adapter pattern is a well-understood software design approach. You define an interface — a contract — and every provider implementation must satisfy that contract. Your application code talks to the interface. The concrete provider sits behind it, invisible to the rest of the codebase.

For payments, the interface covers a small surface area: create a checkout session, fetch a subscription, cancel a subscription, handle an incoming webhook. That's most of what a SaaS needs.

The implementation for each provider lives in its own file. It knows the provider's SDK, its event names, its payload shapes. It translates everything into the normalized interface your app expects.

The normalization layer for webhooks is where this gets valuable. Instead of letting Stripe's customer.subscription.updated or Lemon Squeezy's subscription_updated leak into your handlers, you define a provider-agnostic event shape. Something like a NormalizedSubscriptionEvent with consistent fields — subscription ID, customer ID, status, plan, timestamps — regardless of which provider fired the webhook. Your business logic handles that normalized shape. The adapter does the translation.

This means your subscription lifecycle logic — updating a database row, sending a confirmation email, provisioning access — is written once and tested once. It doesn't care which provider sent the event.

The only requirement for making this work cleanly is discipline at the seam. You have to resist the temptation to let provider-specific types bleed into application code. The adapter boundary needs to be real, not aspirational.

The Universal Payment Adapter — zero-code switching across Stripe, Lemon Squeezy, Polar, Dodo, and Creem via a single env var

Vibe44 ships this adapter pre-built. The directory structure is deliberate: lib/payments/ is the root of all payment logic. Inside it, lib/payments/providers/stripe.ts, lib/payments/providers/lemon-squeezy.ts, lib/payments/providers/polar.ts, lib/payments/providers/dodo.ts, and lib/payments/providers/creem.ts each implement the same interface. The main lib/payments/ index resolves which adapter to use at runtime by reading a single environment variable: PAYMENT_PROVIDER.

Set PAYMENT_PROVIDER=stripe and every checkout call, every subscription query, every webhook handler routes through the Stripe adapter. Set PAYMENT_PROVIDER=lemonsqueezy and the Lemon Squeezy adapter takes over. The call sites in your application — the places where you trigger a checkout, check subscription status, process a webhook — don't change. Not a single line.

This is not a thin wrapper that just renames functions. The adapters handle SDK initialization, request formatting, error normalization, and response mapping. Each adapter knows its provider's quirks: Stripe's idempotency key pattern, Lemon Squeezy's store ID requirements, Polar's subscription tier structure.

The interface itself is typed in TypeScript. If you build a new adapter for a future provider, the type system tells you exactly what you need to implement before you ship a line of untested code.

For founders, this matters at two inflection points. First, when you're deciding which provider to launch with — you can pick the one that fits your current market and switch later without a rewrite. Second, when your business grows into a new region or billing model — you flip PAYMENT_PROVIDER in your environment config, deploy, and the codebase is unchanged.

The practical comparison: building this abstraction yourself, with proper normalization and test coverage, takes several weeks of focused engineering. Vibe44's pre-built adapter is part of a $197 one-time license that covers the full boilerplate — a stack that would cost roughly 480 hours and $36,000 to assemble from scratch, according to our internal build-vs-buy accounting (see Next.js SaaS Boilerplate: Build vs. Buy — The Real 2026 Cost Math for the full breakdown).

How webhook and subscription logic stays provider-agnostic (the normalization layer)?

The normalization layer is the part most developers skip when they first attempt an adapter pattern for payments. They abstract the checkout call, feel good about it, and leave the webhook handlers directly coupled to Stripe's event schema. Six months later, switching providers is still a multi-week project.

Here's how Vibe44's normalization works in prose.

When a webhook arrives — from any provider — it hits a single route handler. That handler reads the PAYMENT_PROVIDER env var, selects the correct adapter, and calls a parseWebhook method on it. Each adapter's parseWebhook does two things: it validates the webhook signature using the provider's specific method (Stripe's constructEvent, Lemon Squeezy's HMAC header check, Polar's own verification, Dodo's signature check, Creem's verification), and it maps the raw event payload into a normalized shape.

The normalized shape is a TypeScript discriminated union. Each variant corresponds to a business event — subscription created, subscription updated, subscription cancelled, payment succeeded, payment failed — not a provider event. Your business logic switches on the normalized event type, not on "customer.subscription.updated" or "subscription_updated".

Subscription status is normalized the same way. Each provider has slightly different terminology for active, trialing, past-due, and cancelled states. The adapter maps these to a shared enum. When your application checks whether a user has an active subscription, it checks the normalized status, not a provider-specific string.

This means the 900 unit tests in Vibe44's test suite can test subscription lifecycle logic without mocking a specific provider. They operate on normalized data. The adapter implementations themselves are tested separately, with provider-specific fixtures. The two layers of testing are cleanly separated because the code is cleanly separated.

HMAC-bound CSRF protection applies at the webhook ingestion layer too, consistent with Vibe44's broader OWASP Top 10 (2025)-aligned security posture. Webhook signatures are validated before any payload is parsed.

Migrating providers without downtime — a safe cutover plan

Switching providers in production is riskier than switching in development. Existing subscribers are on the old provider. You can't just flip the env var and walk away — you need to handle the transition period where some customers are on Stripe subscriptions and new customers are signing up through Lemon Squeezy.

Here's a practical cutover sequence that keeps both providers active during migration.

Start by extending the adapter to support a PAYMENT_PROVIDER_LEGACY env var alongside PAYMENT_PROVIDER. During the migration window, webhook ingestion checks both — validating against the legacy provider's signature method if the primary validation fails. This handles the overlap period where the old provider still fires events for existing subscribers.

Next, migrate existing subscribers. Most providers give you export tooling. You export your subscriber list, create corresponding records in the new provider, then update your database to point each user record at their new provider subscription ID. This is a data migration, not a code change.

Run both adapters in parallel for a billing cycle. Existing subscribers renew on the old provider; new subscribers go through the new one. Your normalized webhook handlers process both without modification, because the normalization layer doesn't care which provider fired the event.

Once existing subscriptions have been migrated or have naturally churned, drop the legacy env var and remove the fallback validation path.

The key insight is that the adapter pattern makes this dual-provider window feasible. Without the normalization layer, running two providers simultaneously means maintaining two sets of webhook handlers, two sets of subscription queries, two sets of status checks. With the adapter, it's an env var and a fallback validation path.

For a comparison of total migration effort with and without an abstraction layer, see Stop Hardcoding Stripe: A Universal Payment Adapter for Next.js (Stripe, Lemon Squeezy, Polar, Dodo, Creem) — and for the full cost math of what building this yourself actually entails, Next.js SaaS Boilerplate: Build vs. Buy — The Real 2026 Cost Math runs the numbers.


Building a payment adapter from scratch takes weeks of careful engineering — defining the interface, implementing five adapters, normalizing webhooks, writing tests that cover the seam. Vibe44 ships it done. You set PAYMENT_PROVIDER= in your environment config and you're running on whichever provider fits your market today.

Switch providers in one env var — see the demo in this video.

Frequently asked questions

1. Does the adapter support usage-based billing, or only flat subscriptions?

The current adapter interface covers flat subscriptions and one-time payments, which covers the majority of SaaS billing models. Polar's usage-based billing features are available through the Polar adapter for teams that need them. Usage-based metering adds surface area to the interface, but the normalization principle is the same — provider-specific metering APIs map to a normalized usage event shape.

2. What happens to existing Stripe subscriptions if I switch to Lemon Squeezy?

Existing subscriptions don't migrate automatically. You need to export subscriber records from Stripe, create corresponding records in Lemon Squeezy, and update your database to point each user at their new subscription ID. During the overlap period, you can run both adapters in parallel by using a legacy provider env var alongside the primary one. The normalized webhook handlers process events from both without code changes.

3. How do I add a fourth payment provider not currently in Vibe44?

Implement the PaymentAdapter interface in a new file under lib/payments/providers/. The TypeScript interface specifies exactly what methods you need: create checkout session, get subscription, cancel subscription, parse webhook. Add a new case to the PAYMENT_PROVIDER resolver in lib/payments/. Your call sites and webhook handlers don't change.

4. Does the normalization layer handle payment failure retries and dunning?

Dunning logic — retrying failed payments, sending overdue notices, cancelling after N failures — is handled by the provider, not by the adapter. The adapter normalizes the resulting events (payment_failed, subscription_past_due, subscription_cancelled) into the standard shape. Your application responds to those normalized events. You configure dunning behavior in each provider's dashboard, not in code.

5. Is the adapter tested across all five providers?

Yes. Vibe44 ships 900 unit tests and 9 E2E suites. The adapter implementations are tested with provider-specific fixtures — real event payloads captured from each provider's webhook test tooling. Business logic tests operate on normalized data and are provider-agnostic. The separation of test layers maps directly to the separation of code layers.

Share this article

Explore More from the Vibe44 Blog

A 3D rendering of a checklist with green checkmarks alongside a dashboard showing an upward trending graph, symbolizing production readiness.
Payments & ProductionJune 9, 2026

The Production-Readiness Checklist: 27 Things Your SaaS Needs Before Launch!

Shipping a Next.js SaaS to real, paying, hostile users takes more than a green build. This checklist enumerates 27 concrete things — across security and data, payments and billing, and observability and testing — that stand between a demo and production. Vibe44 ships most of the 27 by default, backed by RLS across 32 tables, 900 Vitest unit tests, and 9 Playwright E2E suites.

A blueprint of a server rack being scanned by a glowing AI eye.
AI & AgentsJune 13, 2026

How to Optimize Your SaaS for ChatGPT, Claude, Perplexity, etc. in 2026?

Building for AI LLMs like ChatGPT, Claude, Perplexity, etc. isn't about keyword density; it's about making your architecture structurally sound for AI consumption. With ChatGPT processing 250–500M weekly queries and capturing ~17% of global digital query share, SaaS founders who don't architect for answer engines are invisible to the next generation of buyers.

A diagram showing how Vibe44 is built for AI agents, depicting various AI agent logos pointing to a Vibe44 MCP Server, which in turn connects to Vibe44 documentation resources.
AI & AgentsJune 11, 2026

From Idea to Paying Users in a Weekend — An AI-Agent-Driven Building System.

You can build SaaS with AI in a weekend, but only if your agent starts on a real foundation. Vibe44 ships that foundation — AGENTS.md + CLAUDE.md, the Graphify Context Engine (up to 71.5x token cost reduction), a public docs MCP server, and 900 unit tests plus 9 E2E suites that catch agent mistakes before prod.