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-productStart dev webhook forwarding
Use this command for local buyer-flow QA against your hosted Convex dev deployment:
bun run stripe:listen:devIf you want one command that starts the web app, hosted Convex dev sync, and Stripe forwarding together, use:
bun run dev:fullThat helper:
- starts Stripe CLI forwarding to
{NEXT_PUBLIC_CONVEX_SITE_URL}/stripe/webhook - checks the pinned Stripe CLI project
ship-by-sundayand warns withstripe login --project-name ship-by-sundayif the CLI context drifted - captures the current webhook signing secret
- writes that secret into
.env.local - syncs that secret and
STRIPE_SECRET_KEYinto 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:
- buyer calls
POST /api/stripe/checkoutfrom the public CTA, whether they are signed in or signed out - Stripe hosts checkout
- Stripe returns to
/checkout/success - Stripe webhook hits Convex HTTP at
{NEXT_PUBLIC_CONVEX_SITE_URL}/stripe/webhook - 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
/checkout/successsigns the buyer in or creates the account if needed, then claims the purchase when the verified primary email matches the Stripe checkout email- the app redirects into
/start
Health checks
Use these commands before you debug by hand:
bun run stripe:dev:doctor
bun run stripe:dev:verifybun 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:
- checkout completes in Stripe
- Stripe forwards
checkout.session.completed - the webhook grants product access
/checkout/successstops waiting- the buyer reaches
/start
If this breaks
The most common failures are:
- the pinned Stripe CLI project
ship-by-sundayis missing or logged into a different Stripe account thanSTRIPE_SECRET_KEY .env.localand Convex do not match onSTRIPE_WEBHOOK_SECRETorSTRIPE_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-sundayIf you are seeing a paid session without access, start with Troubleshooting Stripe and Convex.