Skip to Content
Stripe Checkout + Webhook

Stripe Checkout + Webhook

The Stripe setup has one job: turn a real paid checkout into a real product-access grant.

The easiest way to break that path is secret drift between the app and Convex.

In local development, this repo also treats the Stripe CLI project name as a guardrail. The pinned local profile is ship-by-sunday, but the runtime source of truth is still STRIPE_SECRET_KEY from apps/web/.env.local.

For production in this repo, the live Stripe webhook should target the Convex HTTP-actions origin, not the app origin:

https://<your-convex-production-deployment>.convex.site/stripe/webhook

If Stripe is pointed at https://<your-app-domain>/api/stripe/webhook while the real handler lives in Convex, Checkout can complete and return to /checkout/success while access grant never happens.

Required Stripe env values

Add these to .env.local:

STRIPE_SECRET_KEY= STRIPE_PRODUCT_ID= STRIPE_PRICE_ID= STRIPE_WEBHOOK_SECRET= APP_BASE_URL=

This repo uses Stripe-hosted Checkout, so there is no public Stripe key in the tracked env template.

Create the product and price

If you do not already have them, create the Stripe product and default price from the repo:

bun run stripe:create-product

Start dev webhook forwarding

Use this command for local buyer-flow QA against your hosted Convex dev deployment:

bun run stripe:listen:dev

If you want one command that starts the web app, hosted Convex dev sync, and Stripe forwarding together, use:

bun run dev:full

That helper:

  • starts Stripe CLI forwarding to {NEXT_PUBLIC_CONVEX_SITE_URL}/stripe/webhook
  • checks the pinned Stripe CLI project ship-by-sunday and warns with stripe login --project-name ship-by-sunday if the CLI context drifted
  • captures the current webhook signing secret
  • writes that secret into .env.local
  • syncs that secret and STRIPE_SECRET_KEY into the active hosted Convex dev env

It also expects .env.local to contain the CONVEX_DEPLOYMENT value that bun run dev:convex writes. In this repo, that same bun run dev:convex sync also needs to leave NEXT_PUBLIC_CONVEX_SITE_URL in .env.local, because Stripe forwards into the Convex HTTP-actions origin instead of the .convex.cloud client origin.

If the listener writes a fresh secret while the app is already running, restart the app before testing checkout.

Use test mode first

Do not start by wiring live mode.

Start in a safe sandbox or test-mode environment, get the full handoff working there, and only then mirror the same setup into production.

The handoff architecture

The starter uses this sequence:

  1. buyer calls POST /api/stripe/checkout from the public CTA, whether they are signed in or signed out
  2. Stripe hosts checkout
  3. Stripe returns to /checkout/success
  4. Stripe webhook hits Convex HTTP at {NEXT_PUBLIC_CONVEX_SITE_URL}/stripe/webhook
  5. Convex verifies the signature and either grants access directly for a signed-in checkout or stores a durable pending paid checkout keyed by the Stripe session
  6. /checkout/success signs the buyer in or creates the account if needed, then claims the purchase when the verified primary email matches the Stripe checkout email
  7. the app redirects into /start

Health checks

Use these commands before you debug by hand:

bun run stripe:dev:doctor bun run stripe:dev:verify

bun run stripe:dev:doctor compares the app Stripe account from apps/web/.env.local against the pinned Stripe CLI project ship-by-sunday. The listener itself still runs from STRIPE_SECRET_KEY, so a CLI mismatch is a local guardrail problem, not a second billing authority.

What success looks like

The happy path is:

  1. checkout completes in Stripe
  2. Stripe forwards checkout.session.completed
  3. the webhook grants product access
  4. /checkout/success stops waiting
  5. the buyer reaches /start

If this breaks

The most common failures are:

  • the pinned Stripe CLI project ship-by-sunday is missing or logged into a different Stripe account than STRIPE_SECRET_KEY
  • .env.local and Convex do not match on STRIPE_WEBHOOK_SECRET or STRIPE_SECRET_KEY
  • production Convex is missing STRIPE_SECRET_KEY, so the webhook cannot construct Stripe even after the signature verifies
  • you changed the webhook secret but did not restart the app
  • Clerk/Convex auth is broken, so the buyer cannot be matched to the right access state

If the doctor says the pinned CLI project is missing or mismatched, repair that exact profile with:

stripe login --project-name ship-by-sunday

If you are seeing a paid session without access, start with Troubleshooting Stripe and Convex.

Last updated on