Production is not running on a Stripe test key
On a production domain the publishable key is pk_live, not pk_test.
- Severity
- high
- Tier
- Passive: runs on any URL
- Category
- Payments
- Fix
- Free, in full, below
- Id
- stripe-test-key-in-prod
What this means
Passing means the Stripe publishable key your production page ships starts with pk_live_. Failing means it starts with pk_test_ on a domain that is clearly production: every checkout it opens runs in test mode, test cards succeed, real cards are refused, and no money moves. The page looks finished. It is not.
How we test it
We fetch your home page and its first-party bundles and look for a Stripe publishable key. Pass is a pk_live_ key, or no key at all. Warn is a pk_test_ key on a domain that looks like staging or a preview. Fail is a pk_test_ key on a production domain. Publishable keys are public by design, so finding one is not itself a finding; only its mode is.
Why it matters
Customers on a test key cannot pay, and nobody notices until launch day. The first real signup is the worst moment to find out: a sports data product in the Fix Kit's own history took its first paying signup and landed the user on a page that said the page was not working, because the Stripe secrets had been set with echo and carried a trailing newline. Mode mismatches fail the same quiet way: no error in your logs, just a customer who gave up.
The fix
Going live in Stripe is a short list of hand steps and one deploy, and it creates real charges, so a human does it deliberately. Module 4's runbook (Step 12) is the source.
In the Stripe dashboard, switch to live mode. Under Settings, Payouts, confirm a bank account is the destination, not a Financial Account; otherwise revenue sits inside Stripe where nobody looks. Configure the Customer Portal again in live mode (Settings, Billing, Customer portal).
Developers, API keys: copy the live secret key into
STRIPE_SECRET_KEYfor the Production environment only. Keep test keys on Preview. Paste it in the hosting dashboard field, or if you must use the CLI:
printf %s 'sk_live_...' | vercel env add STRIPE_SECRET_KEY production
Never echo: it bakes a trailing newline into the value and breaks the Authorization header with an error that reads like a network failure. Set the live publishable key the same way, under whatever name your page reads.
- Mirror the catalogue in live mode with the kit's idempotent setup script, in your own terminal so the key never passes through an AI:
STRIPE_SECRET_KEY='paste-live-key-here' APP_SLUG=YOUR_SLUG npx tsx scripts/setup-stripe-products.mjs
Paste each printed STRIPE_PRICE_*=price_... line into Production exactly as printed.
Register the live webhook at
https://YOUR_DOMAIN/api/webhookfor the seven events listed intemplates/webhook.js, and set the liveSTRIPE_WEBHOOK_SECRETon Production.Make sure every env read trims, so the next person's
echocannot break it:
grep -rnE "process\.env\.(STRIPE|SUPABASE|RESEND|GA4)[A-Z_]*" --include=*.js --include=*.ts --include=*.mjs . | grep -v node_modules | grep -vE "trim\(\)|env\(\"|env\('"
Expect no output.
- Redeploy, wait for Ready, then verify with one request:
curl -s https://YOUR_DOMAIN | grep -o 'pk_[a-z]*_' | sort -u
Expect pk_live_ only. Then buy the smallest plan with your own card and refund it from the dashboard. Expected cost: the card fee on that one charge, about 30c plus 2.9%, which Stripe does not refund.
A deeper runbook for this class is in the Fix Kit, Module 04.
Prompt for your AI
Paste this into the coding agent that built the app.
Search the source and the build output for every Stripe publishable key and every variable that holds one (grep for pk_test_, pk_live_ and the env name the page reads) and show me each file and the key's first eight characters. Tell me which hosting environment (Production, Preview, Development) each STRIPE_* variable is set in, using `vercel env ls`, without printing any value. Run the env read audit from Module 4 step 11 and show me the output; every process.env.STRIPE_* read must go through .trim(). STOP. I will paste the live keys into the Production environment in the hosting dashboard myself and register the live webhook; do not run anything against live mode, and wait for my confirmation. After I confirm and the deploy shows Ready, run once: curl -s https://YOUR_DOMAIN | grep -o 'pk_[a-z]*_' | sort -u Show me the output; expected: pk_live_ and nothing else.
Grade your own URL
Every grade lists this check with its evidence for your site. Two free grades a day.