What payments do in your app
You’ll use payments for two things:- Selling plan tiers — like Small, Medium, or Large one-time purchases
- Selling credit top-ups — like
credits-smallandcredits-large
Pick your payment provider
- Stripe
- LemonSqueezy
- Polar
The most popular choice for developers. You get full control over products, prices, and metadata. If you’ve used Stripe before, you’ll feel right at home.Best for: Teams who want the most familiar developer payment flow.Set up Stripe ->
Key concepts
Checkout URL
This is the payment link your user clicks. You store these as env vars so your UI can use them without hardcoding.Webhook
A webhook is a secure HTTP request your payment provider sends to your app after a successful payment. Each provider has its own endpoint:- Stripe:
https://yourdomain.com/api/payments/stripe - LemonSqueezy:
https://yourdomain.com/api/payments/lemonsqueezy - Polar:
https://yourdomain.com/api/payments/polar
Purchase type
This is the internal label your app uses to know what was bought. The repo supports:plan-small | plan-medium | plan-large | credits-small | credits-large
Your payment provider might call it a “product”, “variant”, or “price” — but inside this app, it always maps to one of those purchase types.
Before you start
Make sure you have these ready:- Your app runs locally
- Better Auth and your database are set up
- You know your production domain
- You’ve picked which provider to use first
Shared env vars
These env vars are used regardless of which provider you choose:| Variable | What it does |
|---|---|
NEXT_PUBLIC_PAYMENT_PROVIDER | Which provider the UI should use |
NEXT_PUBLIC_DEFAULT_MARKETING_PURCHASE_TYPE | Which plan the main marketing CTA represents |
NEXT_PUBLIC_CHECKOUT_URL_TEMPLATE | Fallback main checkout URL |
NEXT_PUBLIC_CHECKOUT_URL_CREDITS_SMALL | Checkout link for the small credit pack |
NEXT_PUBLIC_CHECKOUT_URL_CREDITS_LARGE | Checkout link for the large credit pack |
How the repo handles payments
Here are the key files you should know about:| File | Purpose |
|---|---|
lib/payments/public-config.ts | Shared checkout URLs, labels, prices, credit-pack metadata |
components/(ui-components)/payments/checkout-link.tsx | Hosted checkout buttons in the UI |
app/api/payments/[provider]/route.ts | Shared webhook entrypoint |
lib/payments/providers/* | Provider-specific verification logic |
lib/payments/processor.ts | Shared “payment succeeded” business logic |
Real checkout URLs should live in env vars. Shared purchase display metadata lives in
lib/payments/public-config.ts. UI components should never hardcode raw checkout links.Metadata vs product maps
There are two ways to tell your app what a payment means.- Option A: Metadata (recommended)
- Option B: Product map in env
Store a value like
type=plan-medium directly in the payment provider. This is the cleanest approach because the purchase explains itself — no extra configuration needed.Credits
If the purchase type iscredits-small or credits-large, the repo adds credits automatically after the webhook succeeds.
If it’s a plan like plan-medium, the repo stores the purchase and updates the user’s profile state instead.
Optional Meta Ads attribution
If you run Meta Ads, the repo can trackInitiateCheckout and Purchase events. Only enable this if you actually need it:
Suggested setup order
Using the wrong webhook URL
Using the wrong webhook URL
Double-check that the URL in your provider dashboard matches your actual domain, including the correct path (
/api/payments/stripe, /api/payments/lemonsqueezy, or /api/payments/polar).Forgot to set the provider-specific secret
Forgot to set the provider-specific secret
Each provider needs its own webhook secret env var. Without it, webhook verification fails silently.
Checkout links work but nothing happens after payment
Checkout links work but nothing happens after payment
You probably created checkout links but never connected the webhook. The webhook is what tells your app the payment happened.
Product not mapping to a purchase type
Product not mapping to a purchase type
Make sure your product is mapped — either via metadata (
type=plan-medium) or via the product/variant map in env.Changed .env.local but nothing changed
Changed .env.local but nothing changed
You need to restart your dev server after changing env vars. Next.js doesn’t hot-reload env changes.

