Oolvay ships with a pluggable payment system. One provider is active at a time, controlled by a single environment variable. The rest of your codebase does not change when you switch providers.
Stripe is supported but not covered in these docs as it has not been tested. LemonSqueezy and Razorpay are the documented and tested providers.
| LemonSqueezy | Razorpay | |
|---|---|---|
| Best for | Global sales, automatic tax compliance | Indian market, UPI |
| Hosted checkout | Yes | No, modal only |
| Billing portal | Yes | No, build manually |
| Free trials | Yes | Limited |
| In-place plan change | Yes | No, cancel and resubscribe |
| Proration | No | No |
| Automatic tax / VAT | Yes | No |
| Subscription pause | Yes | Yes |
| UPI, NetBanking | No | Yes |
LemonSqueezy is a strong choice if you want a merchant-of-record arrangement where LemonSqueezy handles VAT and sales tax compliance automatically. This is particularly valuable if you sell globally and want to avoid registering for tax in multiple jurisdictions. Refunds must be issued manually through the LemonSqueezy dashboard as the API does not support them.
Razorpay is the right choice if your users are primarily in India and need UPI or NetBanking support. It should not be used as the sole provider for a global product. The checkout flow is a JavaScript modal rather than a hosted page, which Oolvay handles automatically in the component.
0 / 2,000 characters
CheckoutButtonSet PAYMENT_PROVIDER in your .env.local. Only the keys for the active provider need to be present.
PAYMENT_PROVIDER=lemonsqueezy # lemonsqueezy | razorpay | stripeIf PAYMENT_PROVIDER is not set, the application will throw an error on startup. There is no silent fallback.
All tier definitions and pricing live in config/pricing.ts. This is the first file to edit after choosing a provider.
Three tiers ship by default: starter, pro, and business. The internal keys are used throughout the codebase for access control comparisons and must not be renamed without a global find-and-replace.
export const TIERS: Record<TierKey, TierConfig> = {
starter: { ... }, // free tier
pro: { ... }, // primary paid tier
business:{ ... }, // scale tier
}Tiers must remain ordered from lowest to highest. requireTier() uses
Object.keys(TIERS) to compare tier ranks. Reordering the keys will break
access control.
Change PRICING_CURRENCY to match your provider’s billing currency. This controls the symbol shown on the pricing page.
export const PRICING_CURRENCY = "$" // "$", "€", "£", "₹", etc.PRICE_MAP maps internal price IDs to the corresponding IDs from your active provider’s dashboard. You get these IDs after creating your products and pricing plans inside the provider’s dashboard. The format differs per provider: Razorpay uses plan_xxx, LemonSqueezy uses a numeric variant ID, and Stripe uses price_xxx. The setup page for your chosen provider walks you through exactly where to find them.
export const PRICE_MAP = {
pro_monthly: {
stripe: "price_xxx",
lemonsqueezy: "0000000", // variant ID as a string
razorpay: "plan_xxx",
},
pro_yearly: {
stripe: "price_xxx",
lemonsqueezy: "0000000",
razorpay: "plan_xxx",
},
business_monthly: {
stripe: "price_xxx",
lemonsqueezy: "0000000",
razorpay: "plan_xxx",
},
business_yearly: {
stripe: "price_xxx",
lemonsqueezy: "0000000",
razorpay: "plan_xxx",
},
}Each provider page explains exactly where to find these IDs in the respective dashboard.
Choose the provider that fits your market and complete its setup guide. Once done, users will be able to purchase subscriptions and the application will synchronize payment state automatically through webhooks.