Fix Stripe Checkout redirecting to localhost in production (success_url)
The problem
Checkout worked in dev; in production the session create call failed with:
InvalidRequestError: Not a valid URL: http://localhost:3000/checkout/success
A fallback like process.env.BASE_URL ?? 'http://localhost:3000' silently shipped, and Stripe (rightly) rejects localhost in live mode.
What didn't work
- A ternary on
NODE_ENVwith the prod URL hardcoded — the code was correct, but the env var existed and shadowed it, so the ternary never fired. - Building the URL from
req.headers.host— behind a proxy/ingress that header is sometimes the internal service host (http://web-7f9d:3000), which is worse than localhost. - Retrying with
http://— live mode also requires anhttps://origin, so the same call kept failing with the same error.
The fix
One canonical, validated origin that both dev and prod read:
// lib/stripe.ts
import Stripe from 'stripe';
const BASE_URL = process.env.PUBLIC_BASE_URL;
if (!BASE_URL || !/^https:\/\//.test(BASE_URL)) {
throw new Error(
`PUBLIC_BASE_URL must be set to an https:// URL (got: ${BASE_URL ?? 'unset'})`
);
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-08-01',
});
export async function startCheckout(priceId: string) {
return stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${BASE_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/pricing`,
});
}
Fail-fast at boot means a deploy without the variable dies in seconds with the variable's name, not at the first customer's checkout click.
Why it works
There is exactly one origin string in the system and it is validated before any session is created, so a missing env var becomes a boot error instead of a runtime InvalidRequestError, and dev/prod differ only by configuration — never by a silent fallback.