Skip to main content
Stripe is vibestrap’s default provider — best DX, deepest docs, lowest fees on the typical NA/EU stack. The trade-off: you’re not the merchant of record, so you handle VAT and US sales tax yourself (Stripe Tax solves most of it for a 0.5% surcharge).

Prerequisites

  • A Stripe account (stripe.com) — free, no business entity required for test mode.
  • The Stripe CLI for local webhook forwarding — docs.stripe.com/stripe-cli.
  • A live Postgres database (pnpm db:push already run).

1. Set the active provider

Open src/config/site.ts:
payment: {
  provider: 'stripe' as 'stripe' | 'paddle' | 'lemonsqueezy' | 'creem',
  currency: 'usd',
},
This is already the default. If you’ve changed it, set it back to 'stripe'.

2. Create products + prices in Stripe Dashboard

In the Stripe Dashboard go to Products → Add product. Create one product per scene you want to sell. For the vibestrap scaffold itself you need two prices on the same product (or two separate products):
SceneDescription
Vibestrap promoLimited-time price (e.g. $49 one-time)
Vibestrap standardRegular price (e.g. $99 one-time)
For your own buyer-app you’ll typically add a pro_monthly recurring price, a pro_yearly recurring price, a lifetime one-time price, and 1–4 credits_* one-time prices. Copy each price_… ID — you’ll paste them into env vars next.

3. Set env vars

In .env.local, fill in (names match src/env.ts exactly):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...        # see step 5
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

# vibestrap product (the scaffold itself)
STRIPE_PRICE_VIBESTRAP_PROMO=price_...
STRIPE_PRICE_VIBESTRAP_STANDARD=price_...

# Buyer-app pricing (your customers)
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_YEARLY=price_...
STRIPE_PRICE_LIFETIME=price_...
STRIPE_PRICE_CREDITS_BASIC=price_...
STRIPE_PRICE_CREDITS_STANDARD=price_...
STRIPE_PRICE_CREDITS_PREMIUM=price_...
STRIPE_PRICE_CREDITS_ENTERPRISE=price_...
Only the prices you actually expose in the UI need to be set; the rest can stay empty.

4. Local webhook testing with Stripe CLI

stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
The CLI prints a webhook signing secret like whsec_…. Paste it into STRIPE_WEBHOOK_SECRET in .env.local and restart pnpm dev. Now any test payment you make will deliver an event to your local route, signed with the correct secret. To trigger an event without paying through the UI:
stripe trigger checkout.session.completed

5. Production webhook endpoint

In the Stripe Dashboard: Developers → Webhooks → Add endpoint.
  • URL: https://your-domain.com/api/webhooks/stripe
  • Events (minimum): checkout.session.completed, invoice.paid, customer.subscription.updated, customer.subscription.deleted.
  • After creating, click into the endpoint and copy its Signing secret (whsec_…). Set it as STRIPE_WEBHOOK_SECRET in your production env.

Verify it works

  1. Start dev with stripe listen running.
  2. Open /pricing, click “Get vibestrap” — you should land on Stripe Checkout.
  3. Pay with 4242 4242 4242 4242, any future expiry, any CVC, any ZIP.
  4. You should be redirected back to your successUrl.
  5. Check the database:
    select id, provider, scene, status, amount from payment
    order by created_at desc limit 1;
    
    You should see provider='stripe', status='paid', your test amount.
  6. Re-run stripe trigger checkout.session.completed — no duplicate row, thanks to the payment.invoiceId / sessionId idempotency check.

Common pitfalls

  • Webhook secret mismatch — the secret printed by stripe listen is ephemeral (rotates each session). Don’t paste it into production. Use the one from the Dashboard endpoint.
  • Test vs live confusion — test-mode sk_test_* and live-mode sk_live_* prices are not interchangeable. Each price ID exists in only one mode.
  • Tax not configured — if you sell to the EU and don’t enable Stripe Tax, you’ll owe VAT out of pocket. Either flip Stripe Tax on or use a Merchant of Record (Paddle / Lemon Squeezy).
  • Missing successUrl / cancelUrlcreateCheckout throws if either is empty. They must be absolute URLs (https://…).
  • Subscriptions without subscription_data.metadata — recurring renewals emit invoice.paid events whose metadata is empty. The Stripe provider copies checkout metadata into subscription_data.metadata so renewals carry your userId / scene forward — don’t strip that path.

Official docs