Debugging Stripe Embedded Checkout on adamcherrycomics.dangerouscentaur.com: Reconciling Lambda PaymentIntent vs Frontend Modal Flow
What Was Done
During a routine status check on the adamcherrycomics project, we discovered a critical discrepancy between the documented checkout architecture and the actual runtime behavior. The handoff notes claimed a hosted-redirect Stripe flow due to library constraints, but inspection of the live Lambda function revealed it was already returning PaymentIntent client secrets with ui_mode="embedded" enabled. Meanwhile, the frontend modal JavaScript appeared disconnected from this server-side implementation. This post documents the investigation, findings, and the architectural decisions needed to restore a working end-to-end checkout flow.
Initial Verification
We began by confirming baseline health:
- All pages returning 200: Verified across
/,/about-artist,/shop, and individual product pages onhttps://adamcherrycomics.dangerouscentaur.com/ - Lambda responsiveness: Invoked
adam-cherry-checkoutfunction directly; confirmed it accepts POST requests and returns structured JSON responses - S3 + CloudFront: Assets serving from
dc-sitesbucket through CloudFront distributionE2Q4UU71SRNTMB - DNS: Namecheap CNAME record explicitly routing
adamcherrycomics.dangerouscentaur.com(wildcard shadowing issue from earlier work was resolved)
All infrastructure appeared healthy. The issue was purely in the payment flow logic.
The Discrepancy: What the Docs Said vs. What the Code Did
The agent handoff from 2026-05-21 documented this constraint:
"Stripe modal compliance — your global rule is embedded modal, never redirect, but this site is on hosted-redirect because stripe-python 15.1.0 rejected ui_mode='embedded'. Worth revisiting (pin older stripe lib, or use Elements) to bring it in line with the rest of your sites."
However, examining the actual Lambda function on the EC2 host at /path/to/ACC/lambda/checkout.py revealed the code was already attempting to use embedded mode:
stripe.PaymentIntent.create(
amount=amount_cents,
currency='usd',
ui_mode="embedded",
return_url=return_url,
...
)
This returns a client_secret (format: pi_...), which is the correct server response for an embedded Stripe checkout flow. But the frontend modal JavaScript in the live index.html showed no corresponding handler for this client secret—no Stripe Elements initialization, no embedded checkout mount point.
Root Cause Analysis
The mismatch occurred because:
- Lambda was patched but frontend wasn't: A previous session had updated the Lambda to return PaymentIntent with
ui_mode="embedded"(likely to comply with the global embedded-modal policy), but the HTML/JS wasn't updated to consume this response. - Missing Stripe.js initialization: The live
index.htmllacked the required<script src="https://js.stripe.com/v3/">tag. - No modal mount logic: Even if Stripe.js was present, there was no JavaScript to call
stripe.initEmbeddedCheckout()or mount the checkout form into a DOM element. - CORS issues compounded it: Early Lambda invoke attempts failed on preflight OPTIONS, masking the real problem (which was: "even if the request succeeds, the frontend can't handle the response").
Technical Infrastructure Details
Lambda Configuration
Function name: adam-cherry-checkout
Handler: checkout.py::lambda_handler
Runtime: Python 3.11
Region: Inferred from request context via os.environ.get('AWS_REGION', 'us-west-2')
Environment variables (relevant):
STRIPE_SECRET_KEY— sourced from Secrets Manager or CloudFormation parameterSTRIPE_PUBLISHABLE_KEY— same originRETURN_URL— should be derived from incoming request to support multi-origin (e.g.,https://{origin}/checkout-complete)
API Gateway Configuration
Resource: /checkout
Method: POST
Integration: Lambda adam-cherry-checkout
CORS configuration had to be updated to allow requests from the staging subdomain:
- Allowed Origins:
https://adamcherrycomics.dangerouscentaur.com,https://adamcherrycomics.dangerouscentaur.com:*(for dev with local port variants) - Allowed Headers:
Content-Type,X-Amz-Date,Authorization,X-Api-Key - Allowed Methods:
POST,OPTIONS
S3 / CloudFront Layout
Live source: s3://dc-sites/adamcherrycomics/index.html
Distribution ID: E2Q4UU71SRNTMB
CloudFront invalidation paths: /adamcherrycomics/index.html or /adamcherrycomics/* as needed
Key Decisions & Architecture Rationale
Why Embedded Checkout (Not Redirect)
Adam's site follows your global policy: embedded modal, never redirect. Embedded checkout provides a better UX (no page navigation) and reduces cart abandonment. The PaymentIntent + ui_mode="embedded" approach is the modern Stripe SDK pattern (as of Stripe.js v3).
Multi-Origin CORS Strategy
The RETURN_URL in Lambda was hardcoded, which breaks when the site is accessed from different origins (prod, staging, dev). The fix was to derive it from the incoming HTTP request header:
origin = event['headers'].get('origin', 'https://adamcherrycomics.dangerouscentaur.com')
return_url = f"{origin}/checkout-complete"
This allows the same Lambda function to serve multiple domains or subdomains without redeploy.
Why the Handoff Was Misleading
The earlier decision to document a "hosted-redirect" flow was likely made when the Lambda did use redirect mode. A subsequent patch switched it to embedded, but documentation wasn't updated. This is why live-code inspection is critical—handoffs can drift.
What's Next
The immediate action items are:
- Update frontend
index.html