Debugging Stripe Embedded Checkout on adamcherrycomics.dangerouscentaur.com: PaymentIntent vs. Session Redirect
What Was Done
During a live-site troubleshooting session on adamcherrycomics.dangerouscentaur.com, we discovered that the Stripe checkout flow was returning a PaymentIntent client secret instead of the expected hosted redirect URL. The handoff documentation claimed the Lambda had been converted to use ui_mode="embedded", but the actual behavior contradicted both the documentation and the site's global architectural requirement: always use embedded modal, never redirect.
This post documents the investigation, the architectural misalignment discovered, and the cleanup path forward.
Technical Details: The Investigation
Initial Setup
adamcherrycomics is a Django-backed static-hosting site running on:
- Frontend: S3 bucket
dc-sites+ CloudFront distributionE2Q4UU71SRNTMB - Domain:
adamcherrycomics.dangerouscentaur.com(CNAME via Namecheap) - Checkout Lambda:
adam-cherry-checkoutin AWS profilefinalconstructclean - API Gateway:
n0nh1zscq4(regional, multi-origin CORS) - Products: 12 buyable items ranging $10–$40
The Discrepancy
Inspection of the live Lambda function revealed:
import stripe
stripe.api_key = os.environ['STRIPE_SECRET_KEY']
# Current code (not as documented)
intent = stripe.PaymentIntent.create(
amount=amount_cents,
currency='usd',
metadata={'product_id': product_id}
)
return {
'statusCode': 200,
'body': json.dumps({
'client_secret': intent.client_secret
})
}
The handoff claimed ui_mode="embedded" was already in place, but the actual implementation was creating a bare PaymentIntent and returning only the client secret. This is a valid Stripe pattern (Elements-based flow), but it contradicts:
- The documented handoff (which said hosted redirect was in use)
- The global architectural rule (embedded modal only)
- The frontend modal JavaScript, which appeared to expect a different response shape
Frontend Modal Inspection
Checking the live index.html snapshot from S3 dc-sites:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
document.getElementById('checkout-btn').addEventListener('click', async () => {
const response = await fetch('/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: productId })
});
const { client_secret } = await response.json();
// Elements flow — expects client_secret for confirmPayment()
const { error } = await stripe.confirmPayment({
elements: elements,
clientSecret: client_secret,
confirmParams: { return_url: RETURN_URL }
});
});
</script>
The frontend does expect a client_secret response, which matches the Lambda's current behavior. However, this pattern—creating a bare PaymentIntent and having the client confirm it—is a non-embedded flow. It requires the client to handle payment details and post-confirmation redirects.
Architecture & Decision Points
Why "Embedded Modal Only"?
The global architecture rule exists to:
- Reduce PCI scope: Stripe-hosted checkout means card data never touches our servers or frontend logic.
- Consistency: All sites use the same Stripe integration pattern, reducing maintenance surface.
- UX stability: No JavaScript errors from client-side payment confirmation can break the flow.
True embedded modal means the Lambda returns a Stripe Checkout Session URL with ui_mode="embedded", and the frontend opens it in an iframe or redirect—Stripe handles everything.
Current Code Reality
The current implementation (bare PaymentIntent + client-side confirmPayment()) is valid but requires:
- Complex frontend state management (handling confirmation, redirects, error recovery).
- Proper
return_urlderivation in the Lambda (to support multi-origin deployments). - CORS preflight handling for the POST request.
These are all in place, but the pattern doesn't match the architectural standard.
Infrastructure Details
Lambda Configuration
- Function name:
adam-cherry-checkout - Runtime: Python 3.11
- Handler:
lambda_function.lambda_handler - Environment variables:
STRIPE_SECRET_KEY(masked in console)STRIPE_PUBLISHABLE_KEY(visible, used for return_url derivation)
- Timeout: 15 seconds (sufficient for Stripe API calls)
CloudFront & API Gateway
- CloudFront distribution:
E2Q4UU71SRNTMB(S3 origin) - API Gateway:
n0nh1zscq4, regional, with CORS rules allowingadamcherrycomics.dangerouscentaur.comandlocalhost - Lambda integration: POST
/checkoutendpoint integrated with API Gateway, returning structured JSON
Stripe Account Configuration
adamcherrycomics uses a Stripe account within the finalconstructclean AWS profile. Key settings verified:
- Publishable key available in environment (no secrets leak in frontend)
- Secret key restricted to Lambda execution role
- No Stripe Connect account configured (simple direct payments only)
Key Decisions & Path Forward
Option A: Standardize to Stripe Checkout Session (Embedded Modal)
Pros:
- Matches global architecture rule
- Simpler frontend: one iframe