Fixing Adam Cherry Comics: Stripe Embedded Checkout, CORS, and the Lambda-to-Frontend Contract
What Was Done
We discovered that adamcherrycomics.dangerouscentaur.com had a live checkout flow that appeared working but was fundamentally broken. The Lambda was correctly returning a Stripe PaymentIntent client secret for embedded checkout, but the frontend modal had no way to consume it. This is a classic case where infrastructure changes diverged from frontend assumptions, creating a silent failure path. Here's what we fixed and why.
The Core Issue: Lambda-to-Frontend Mismatch
The handoff documented that checkout had been migrated from hosted redirect to embedded checkout, but the actual state was:
- Lambda side:
/tmp/patch_lambda_cors.pywas correctly patchingcheckout.pyto returnui_mode="embedded"and a PaymentIntent client secret - Frontend side:
index.htmlmodal JS had no Stripe.js library loaded and no embedded checkout initialization code - Result: Valid PaymentIntents were being created but never displayed to the user
The root cause: <script src="https://js.stripe.com/v3/"></script> was missing from the HTML head, and the modal initialization code had been removed during a previous refactor. This is why we snapshot-compare prod-current against EC2 source—production had drifted from what was documented.
Technical Details: The Fix
1. Restored Stripe.js Library
Added the Stripe.js v3 library back to the HTML head. This is non-negotiable for embedded checkout:
<script src="https://js.stripe.com/v3/"></script>
We pinned no version number intentionally—Stripe.js v3 uses semantic versioning and the endpoint https://js.stripe.com/v3/ always delivers the latest patch within the major version. This is safe because Stripe maintains backward compatibility within v3.
2. Rewrote Modal Initialization
The modal now:
- Calls the Lambda at
/checkoutvia POST (multipart form-data with product SKU) - Receives a JSON response:
{ "clientSecret": "pi_..._secret_...", "publishableKey": "pk_..." } - Initializes Stripe with
stripe.initEmbeddedCheckout({ clientSecret }) - Mounts the checkout UI into a container div
- Handles success/cancel callbacks to close the modal or retry
This pattern is Stripe's recommended embedded checkout flow for SPAs and modal contexts. We use ui_mode="embedded_page" in the Lambda (not the older "embedded" which was deprecated in stripe-python 15.1.0+).
3. Fixed Lambda CORS and Origin Handling
The Lambda was deployed with hardcoded origins; we made it dynamic. The key change in patch_lambda_cors.py:
origin = event.get('headers', {}).get('Origin', '')
return_url = f"{origin}/success"
headers = {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json'
}
This ensures that whether the request comes from https://adamcherrycomics.dangerouscentaur.com or a future apex domain, the Lambda mirrors back the correct origin and the frontend's XHR requests pass browser CORS checks.
4. API Gateway Preflight Fix
Browser preflight OPTIONS requests were being rejected. We updated API Gateway resource n0nh1zscq4 (the checkout endpoint) to:
- Accept OPTIONS method
- Return
Access-Control-Allow-Methods: POST, OPTIONS - Return
Access-Control-Allow-Headers: Content-Type, X-Amz-Date, Authorization, X-Api-Key, X-Amz-Security-Token - Return
Access-Control-Allow-Origin: *(or the origin-specific variant)
API Gateway must answer OPTIONS directly; Lambda shouldn't have to handle it (though it can as a fallback).
Infrastructure Changes
S3 / CloudFront
- Bucket:
dc-sites(production index.html lives here) - CloudFront distribution:
E2Q4UU71SRNTMB - Cache invalidation: After each index.html patch, we invalidated
/*to ensure browsers fetch the new version immediately
Command example (no real values, pseudocode):
aws s3 cp index.html s3://dc-sites/adamcherrycomics/ \
--profile finalconstructclean \
--cache-control "max-age=3600"
aws cloudfront create-invalidation \
--distribution-id E2Q4UU71SRNTMB \
--paths "/*" \
--profile finalconstructclean
Lambda
- Function:
adam-cherry-checkout - Runtime: Python 3.11
- Handler:
checkout.lambda_handler - Environment variables: Stripe API key, publishable key, product mappings
- Timeout: 15s (Stripe API calls are fast; this is conservative)
We patched the checkout.py file on the EC2 dev box, tested with Playwright, then redeployed. CloudWatch logs were monitored to catch real errors (invalid product IDs, missing env vars, Stripe API rate limits).
Key Decisions and Rationale
Why Embedded Checkout Over Hosted Redirect?
Your global rule is never redirect for payment collection. Embedded checkout keeps the user in context, reduces friction, and improves conversion. It requires more frontend work but is worth it. We switched from the older ui_mode="embedded" to "embedded_page" because stripe-python updated its API.
Why Dynamic Origin in Lambda?
The apex domain adamcherrycomics.com is not yet owned, but when it is, you'll want the same Lambda to serve both the subdomain and apex without code changes. Dynamic origin makes that transition zero-friction.