```html

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 on https://adamcherrycomics.dangerouscentaur.com/
  • Lambda responsiveness: Invoked adam-cherry-checkout function directly; confirmed it accepts POST requests and returns structured JSON responses
  • S3 + CloudFront: Assets serving from dc-sites bucket through CloudFront distribution E2Q4UU71SRNTMB
  • 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:

  1. 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.
  2. Missing Stripe.js initialization: The live index.html lacked the required <script src="https://js.stripe.com/v3/"> tag.
  3. 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.
  4. 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 parameter
  • STRIPE_PUBLISHABLE_KEY — same origin
  • RETURN_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:

  1. Update frontend index.html