```html

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 distribution E2Q4UU71SRNTMB
  • Domain: adamcherrycomics.dangerouscentaur.com (CNAME via Namecheap)
  • Checkout Lambda: adam-cherry-checkout in AWS profile finalconstructclean
  • 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:

  1. The documented handoff (which said hosted redirect was in use)
  2. The global architectural rule (embedded modal only)
  3. 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_url derivation 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 allowing adamcherrycomics.dangerouscentaur.com and localhost
  • Lambda integration: POST /checkout endpoint 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