Payments
Stripe integration for paid listings, promotions, and sponsorships.
DirectoryKit uses Stripe by default. You can also switch to Lemon Squeezy, Paddle, or disable payments entirely — controlled in payments.config.ts.
Earlier versions of this boilerplate used Polar — that is no longer the case. The code and docs now target Stripe. Ignore any older tutorial that mentions Polar.
What Stripe handles
Three separate paid flows:
| Flow | Price model | Config file |
|---|---|---|
| Premium listing | One-time ($15 default) | config/plans.config.ts |
| Promotion placements | Recurring monthly ($19–$69) | config/advertising.config.ts |
| Sponsor subscriptions | Recurring monthly | config/advertising.config.ts |
Each flow has its own Stripe Product/Price. You create them in Stripe and paste the Price IDs into env vars.
Step 1 — Create a Stripe account
Sign up at stripe.com. You can build and test everything in Test mode (the toggle in the top-right of the dashboard) before switching to Live.

Step 2 — Copy your API keys
Developers → API keys.
- Publishable key — safe for the browser. Not needed by this project (all charges are server-side).
- Secret key — server-only. This is what we need.

Paste into .env.local:
STRIPE_SECRET_KEY=sk_test_...Secret keys start with sk_test_ or sk_live_. They never belong in Git. .env.local is already .gitignored.
Step 3 — Create the Premium product
- Products → Add product
- Name: Premium
- Price: $15, type One-time
- Save

On the product page, find the Price ID (starts with price_) and copy it:

Paste into .env.local:
STRIPE_PRICE_ID_PREMIUM=price_...Or let AI walk you through creating every product you need:
I'm setting up Stripe for my DirectoryKit directory. Based on my config files, tell me exactly which Stripe products and prices I need to create in the Stripe Dashboard.
Read:
- config/plans.config.ts — for listing plans (one-time charges)
- config/advertising.config.ts — for promotion placements and sponsor subscriptions (recurring monthly)
For each price I need to create, give me:
- Product name (shown to users at checkout)
- Price type (one-time or recurring monthly)
- Price amount (from my config)
- Which env var to put the Price ID into
Then generate a complete .env.local block with all STRIPE_PRICE_ID_* variables set to placeholders I'll replace after creating the products.
Finally, tell me what Stripe events the webhook at /api/webhooks/stripe expects, so I subscribe to the right ones.
Repeat for promotion placements, each with a monthly recurring price:
STRIPE_PRICE_ID_PROMO_BANNER=price_...
STRIPE_PRICE_ID_PROMO_CATALOG=price_...
STRIPE_PRICE_ID_PROMO_DETAIL=price_...
STRIPE_PRICE_ID_PARTNER_SUBSCRIPTION=price_...Step 4 — Set up the webhook
Stripe sends events to your app when a payment completes. Without this, users pay but the order is never marked "paid".
For production
- Developers → Webhooks → Add endpoint
- Endpoint URL:
https://your-domain.com/api/webhooks/stripe - Events to listen to:
checkout.session.completed,invoice.paid,customer.subscription.updated,customer.subscription.deleted

- Click Add endpoint → copy the Signing secret (starts with
whsec_)

Paste into .env.local:
STRIPE_WEBHOOK_SECRET=whsec_...For local development
Stripe can't reach your localhost. Use the Stripe CLI:
# 1. install Stripe CLI (macOS)
brew install stripe/stripe-cli/stripe
# 2. log in
stripe login
# 3. forward events to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
The CLI prints a local webhook secret (also starts with whsec_) — use that one in .env.local for development.
Step 5 — Test a purchase
- Run
pnpm dev - In another terminal:
stripe listen --forward-to localhost:3000/api/webhooks/stripe - Go to
/submitand pick Premium - Use Stripe's test card:
4242 4242 4242 4242, any future date, any CVC - Submit → you should land on the success page, and the webhook terminal should show events
Switching providers (optional)
Edit config/payments.config.ts:
export const paymentsConfig = {
provider: 'stripe', // 'stripe' | 'lemonsqueezy' | 'paddle' | 'none'
testMode: false,
};Each provider needs its own env vars and webhook handler.
Free-only mode
export const paymentsConfig = {
provider: 'none',
testMode: false,
};This disables all paid flows. Only the free plan stays on /pricing. Stripe-related env vars can stay empty.
Going live
When you're ready for real money:
- Toggle Test mode → Live mode in Stripe
- Re-create products and copy the live Price IDs
- Re-create the webhook endpoint with the live URL
- Replace your env vars in Vercel with the live keys (
sk_live_...,whsec_...from live webhook) - Redeploy
See also
- Plans Config — shape of the plans
- Advertising Config — promotion pricing
- Webhook Routes — what the Stripe webhook does
- Stripe testing docs