SaaSyBase
SaaSyBase

Payments

SaaSyBase supports four payment providers through a unified abstraction. Switch between them with an environment variable — your business logic stays the same regardless of which provider processes the charges. The runtime supports richer multi-provider pricing data than the current admin plan modal exposes, so this page calls out both the underlying model and the current UI reality.

Note

If you are not deeply technical, use this rule: pick one payment provider first, finish its env vars and webhook setup, confirm a test checkout works, and only then compare advanced features like coupons, proration, or customer portals.

How the Multi-Payment System Works

Like the auth system, payments use a provider pattern: an interface defines what every provider must implement, and a factory resolves the active provider from your environment at runtime. Your app code calls the abstraction — never a provider SDK directly.

.env.local
PAYMENT_PROVIDER="stripe"   # Options: "stripe", "paystack", "paddle", "razorpay"

All providers share a common lifecycle: checkout → webhook → subscription activation. New transactions are routed to the active provider; existing transactions are handled by whichever provider originally processed them (stored in the paymentProvider field).

Warning

Never import provider SDKs (like stripe) directly in your business logic. Always use PaymentProviderFactory.getProvider() from lib/payment/factory.
ProviderPreferred webhook endpointExtra alias routesNotes
Stripe/api/webhooks/payments/api/webhooks/stripe and /api/stripe/webhookStripe is the only provider with both centralized and multiple explicit aliases.
Paystack/api/webhooks/payments/api/webhooks/paystackUses Paystack signature detection in the centralized router.
Paddle/api/webhooks/payments/api/webhooks/paddleCentralized ingress is still the preferred production setup.
Razorpay/api/webhooks/paymentsNo dedicated alias route is currently shippedRazorpay webhook traffic should point at the centralized endpoint.

Stripe

Tip

Choose Stripe when you want the most complete billing feature set in this repo: native discounts, proration support, customer portal, and the strongest overall lifecycle coverage.

Official docs: Stripe docs and Stripe CLI docs.

Environment variables

.env.local
PAYMENT_PROVIDER="stripe"
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."       # Supports comma-separated for rotation
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."

Webhook setup

Endpoint: /api/webhooks/payments (preferred centralized endpoint) or /api/webhooks/stripe / /api/stripe/webhook.

Local testing:

stripe listen --forward-to localhost:3000/api/stripe/webhook

Copy the printed secret into STRIPE_WEBHOOK_SECRET.

Recommended webhook events:

  • checkout.session.completed
  • checkout.session.async_payment_succeeded
  • invoice.payment_succeeded / invoice.payment_failed
  • invoice.upcoming (renewal reminder emails)
  • customer.subscription.created / updated / deleted
  • charge.refunded (optional)
  • charge.dispute.* (optional)

Customer portal

Enable the Stripe Customer Portal in Stripe Dashboard → Settings → Billing → Customer Portal. Without this, the "Manage payment" button in the user dashboard returns an error.

Paystack

Tip

Choose Paystack when your product is centered on the markets it supports best and you are comfortable with a thinner provider feature surface than Stripe.

Official docs: Paystack docs.

Environment variables

.env.local
PAYMENT_PROVIDER="paystack"
PAYSTACK_SECRET_KEY="sk_live_..."
PAYSTACK_WEBHOOK_SECRET=""              # Optional — falls back to PAYSTACK_SECRET_KEY
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY="pk_live_..."

Key details

  • Webhook endpoints: Prefer /api/webhooks/payments; /api/webhooks/paystack is the provider-specific alias.
  • Recommended webhook events: charge.success, subscription.create, subscription.not_renew, subscription.disable, invoice.create, invoice.update, invoice.payment_failed, and refund.processed.
  • Default currency: NGN. Supported: NGN, GHS, ZAR, KES, USD (USD requires merchant approval).
  • Pricing model: Uses plan_code as the price ID. Subscriptions pass the plan code; one-time payments pass the raw amount.
  • Cancel at period end: Paystack has no native cancel-at-period-end. The app implements a workaround: it sets a flag in the database on cancel, then cancels in Paystack before the next charge fires on invoice.created.
  • Manage payment: Uses the subscription's hosted short_url as a best-effort management page rather than a Stripe-style portal.

Paddle

Tip

Choose Paddle when you want merchant-of-record billing and a strong hosted subscription-management flow.

Official docs: Paddle developer docs.

Environment variables

.env.local
PAYMENT_PROVIDER="paddle"
PADDLE_API_KEY="pat_live_..."
PADDLE_WEBHOOK_SECRET="..."
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN="..."
PADDLE_ENV="sandbox"                    # or "production"
NEXT_PUBLIC_PADDLE_ENV="sandbox"

# Only needed when provider-side catalog sync is enabled
PAYMENT_AUTO_CREATE="true"
PADDLE_DEFAULT_TAX_CATEGORY="standard"

The minimal Paddle setup is five values: PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, NEXT_PUBLIC_PADDLE_CLIENT_TOKEN, PADDLE_ENV, and NEXT_PUBLIC_PADDLE_ENV.

Everything else is advanced. Use PAYMENT_AUTO_CREATE plus PADDLE_DEFAULT_TAX_CATEGORY only if you want SaaSyBase to create Paddle catalog objects for seeded plans, and keep overrides like PADDLE_CURRENCY, PADDLE_WEBHOOK_TOLERANCE_SECONDS, or PADDLE_DEBUG_SUBSCRIPTION_UPDATES for exception cases.

Razorpay

Tip

Choose Razorpay when your billing flow is India-focused and redirect-first checkout is acceptable for your product.

Official docs: Razorpay docs.

Environment variables

.env.local
PAYMENT_PROVIDER="razorpay"
RAZORPAY_KEY_ID=""
RAZORPAY_KEY_SECRET=""
RAZORPAY_WEBHOOK_SECRET=""
NEXT_PUBLIC_RAZORPAY_KEY_ID=""
RAZORPAY_CURRENCY="USD"                 # Optional (default: INR)

Key details

  • Webhook endpoint: Razorpay uses the centralized /api/webhooks/payments route. A dedicated Razorpay alias route is not currently shipped.
  • Recommended webhook events: payment_link.paid, payment.captured, payment.failed, refund.processed or payment.refunded, plus subscription.activated, subscription.updated, subscription.cancelled, and subscription.halted.
  • Checkouts are redirect-based: one-time payments use Payment Links, subscriptions use the Subscriptions API.
  • Daily subscription constraint: Razorpay requires recurringIntervalCount >= 7 for daily subscriptions.
  • Razorpay Offers: Set RAZORPAY_ENABLE_OFFERS=trueand embed an offer ID in the coupon's description field (razorpayOfferId=offer_ABC123).

Provider Feature Matrix

This comparison is based on the shipped provider implementations and current provider metadata. The older table mixed roadmap items with live runtime behavior, which was especially misleading around checkout UX.

Stripe

The most complete integration in the repo, with native discounts, proration, customer portal, disputes, and trial periods.

Default USD

Coupons

Supported

Provider-native coupons and promotion codes

Discount objects and promotion-code flows are handled directly by Stripe.

Subscription changes

Supported

Inline plan changes are supported

Upgrades and downgrades can stay in the active subscription lifecycle.

Proration preview

Supported

Preview and bill prorations in provider

The provider can calculate proration before you finalize the plan change.

Manage payment

Supported

Hosted customer portal

Customers can manage cards, invoices, and subscriptions in Stripe-hosted UI.

Checkout flow

Supported

Embedded checkout elements / Checkout Sessions

Stripe has the broadest checkout coverage in the shipped integration today.

Implementation notes

  • Refunds, invoices, receipts, disputes, and trial periods are all implemented in the provider layer.

Paystack

Strong fit for Africa-focused billing, with hosted subscription management and app-level fallbacks where the API is thinner.

Default NGN

Coupons

Partial

In-app discounts only

No native coupon API; discounts are applied in SaaSyBase.

Subscription changes

Partial

Cancel + recreate flow

The provider does not offer native inline plan switching.

Proration preview

Not shipped

Not shipped

Proration is not supported natively.

Manage payment

Partial

Hosted subscription manage link

Uses the provider subscription management URL rather than a full portal.

Checkout flow

Partial

Inline popup / hosted checkout

The current docs now avoid calling this Stripe-style embedded elements.

Implementation notes

  • Cancel-at-period-end is handled with an invoice-created workaround before the next renewal charge.
  • Trial periods are not shipped in the Paystack provider implementation.

Paddle

Merchant-of-record billing with strong subscription management and proration support, centered on Paddle Billing checkout flows.

Default USD

Coupons

Supported

Provider-native discounts and codes

Paddle discount objects can be used without relying on app-only fallbacks.

Subscription changes

Supported

Provider-backed subscription updates

Plan changes stay inside Paddle Billing instead of manual cancel-and-recreate flows.

Proration preview

Supported

Proration preview and billing are implemented

The shipped provider layer includes proration-aware upgrade and billing support.

Manage payment

Supported

Hosted customer portal

Customers are sent to Paddle-hosted subscription management screens.

Checkout flow

Partial

Hosted Paddle checkout

The shipped integration relies on the Paddle checkout flow and the default payment link at /paddle/pay.

Implementation notes

  • Refunds are implemented. The provider capability list does not currently advertise native trial-period support.

Razorpay

Good subscription coverage for India-focused products, but the shipped checkout path is still redirect-first and some lifecycle tooling remains narrower than Stripe.

Default INR

Coupons

Partial

In-app discounts only

Offer IDs can be wired in, but there is no Stripe-style native coupon flow.

Subscription changes

Supported

Subscription update support is implemented

The provider layer supports updating active subscriptions without recreating them.

Proration preview

Partial

Implementation gap remains

The provider includes proration/update code paths, but proration preview currently throws not implemented.

Manage payment

Partial

Hosted subscription short_url

Management is exposed through the provider short URL rather than a full customer portal.

Checkout flow

Partial

Redirect-first today

One-time payments use Payment Links and subscriptions use subscription short URLs in the shipped flow.

Implementation notes

  • Native cancel-at-period-end is supported in the provider capability list.
  • The provider source mentions embedded checkout as a goal, but the current repo still ships redirect-first flows.

Why the checkout row changed

The old matrix used a single “Inline checkout elements” yes/no row. That was too coarse for the repo as it exists today, because some providers use hosted popups or redirect-first flows rather than Stripe-style embedded elements. The new cards describe the shipped flow directly.

If you are choosing a provider for the first time, also read Pricing & Coupons and Webhooks. Those pages explain the user-facing tradeoffs and the production webhook setup that the provider matrix alone does not cover.

Currency System

The app resolves the active currency using a priority chain. The first non-empty value wins:

PrioritySourceScope
1Provider-specific env var (PADDLE_CURRENCY, PAYSTACK_CURRENCY, RAZORPAY_CURRENCY)Per-provider override
2Admin setting: DEFAULT_CURRENCYDB-backed default, set in admin settings
3PAYMENTS_CURRENCY env varEnvironment fallback
4Provider defaultNGN for Paystack, INR for Razorpay, USD for Stripe/Paddle

Multi-currency pricing

Plans support per-provider localized pricing via the PlanPrice model. This allows different prices in different currencies for each provider simultaneously — for example, $10 USD on Stripe and ₦15,000 NGN on Paystack for the same plan.

Note

The current admin plan modal does not expose full PlanPrice CRUD management. Today it manages the base plan fields, recurring cadence, team/token metadata, and advanced external provider price ID overrides. If you need full localized price-row management, use sync tooling, Prisma Studio, seed data, or build additional admin UI.

Plan Catalog Sync

Seeded plans automatically receive provider-generated price IDs during npx prisma db seedwhen catalog auto-create is enabled. You don't need to hand-copy price IDs from your payment dashboard.

.env.local
PAYMENT_AUTO_CREATE="true"

When enabled, saving a plan without a provider price ID auto-creates catalog objects on the configured provider. The manual price ID field in the admin plan form is an advanced override — leave it blank for the normal flow.

This is the part the shipped admin UI supports well today: core plan data plus provider catalog sync and imported external IDs, not a full multi-row localized pricing editor.

Centralized Webhook Endpoint

/api/webhooks/payments is the preferred single endpoint for all payment providers. It auto-detects the provider from the request signature header:

HeaderProvider
stripe-signatureStripe
x-paystack-signaturePaystack
paddle-signaturePaddle
x-razorpay-signatureRazorpay

Provider-specific routes also exist: /api/webhooks/stripe, /api/stripe/webhook, /api/webhooks/paystack, and /api/webhooks/paddle. Razorpay currently relies on the centralized route only. The centralized endpoint is recommended because it simplifies configuration — one URL covers all providers.

Note

Webhook secrets support comma-separated rotation. For example: STRIPE_WEBHOOK_SECRET="whsec_primary,whsec_rotating". The app tries each secret in order until one verifies.

The dedicated Webhooks page now lists recommended events for Stripe, Paystack, Paddle, and Razorpay in one place, so you do not have to infer the non-Stripe event set from implementation details.

Invoices & Refund Receipts

SaaSyBase generates PDF invoices and refund receipts server-side using pdf-lib. These work for all providers regardless of their native invoicing support.

  • Invoices include site branding, invoice number, bill-to details, plan info, coupon breakdown, and payment summary.
  • Refund receipts include refund ID, original transaction reference, refunded vs. original amount, and applied coupons.

Coupon System

The app includes a full coupon engine with provider-aware discount handling:

  • Discount types: Percent-off and amount-off.
  • Duration control: Once, repeating (N months), or forever.
  • Plan restrictions: Limit coupons to specific plans.
  • Usage limits: Max redemptions and time-bound availability.
  • Provider sync: Coupons auto-sync to providers that support native coupons (Stripe, Paddle). For others (Paystack, Razorpay), discounts are applied in-app.
PagePathDescription
Admin/admin/couponsCreate, edit, activate/deactivate coupons
User/dashboard/couponsView redeemed coupons and pending redemptions

Adding New Providers

Start with the public Adding Payment Providers guide, then use the deeper repository guide at docs/adding-payment-providers.mdwhen you are implementing the provider. In short: implement the PaymentProviderinterface, register it, add webhook routing/signature detection, wire any client-side requirements, and write tests.