Skip to main content

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.

Not Polar

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:

FlowPrice modelConfig file
Premium listingOne-time ($15 default)config/plans.config.ts
Promotion placementsRecurring monthly ($19–$69)config/advertising.config.ts
Sponsor subscriptionsRecurring monthlyconfig/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.

Stripe dashboard in test mode

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.

Stripe API keys page

Paste into .env.local:

STRIPE_SECRET_KEY=sk_test_...
Never commit secret keys

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

  1. Products → Add product
  2. Name: Premium
  3. Price: $15, type One-time
  4. Save

Creating the Premium product in Stripe

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

Stripe product page with Price ID highlighted

Paste into .env.local:

STRIPE_PRICE_ID_PREMIUM=price_...

Or let AI walk you through creating every product you need:

AI Prompt· Plan my Stripe products and prices

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

  1. Developers → Webhooks → Add endpoint
  2. Endpoint URL: https://your-domain.com/api/webhooks/stripe
  3. Events to listen to: checkout.session.completed, invoice.paid, customer.subscription.updated, customer.subscription.deleted

Stripe Add webhook endpoint form

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

Stripe webhook signing secret

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

Stripe CLI forwarding webhooks to localhost

The CLI prints a local webhook secret (also starts with whsec_) — use that one in .env.local for development.

Step 5 — Test a purchase

  1. Run pnpm dev
  2. In another terminal: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  3. Go to /submit and pick Premium
  4. Use Stripe's test card: 4242 4242 4242 4242, any future date, any CVC
  5. 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:

  1. Toggle Test mode → Live mode in Stripe
  2. Re-create products and copy the live Price IDs
  3. Re-create the webhook endpoint with the live URL
  4. Replace your env vars in Vercel with the live keys (sk_live_..., whsec_... from live webhook)
  5. Redeploy

See also