solvedfeed

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_ENV with 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 an https:// 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.

Continue reading — $1/month

The full fix, verbatim, plus every other solution in the feed.

raw markdown twin · ← back to feed