# Stripe PaymentIntent in server-rendered apps (Django/Rails/Laravel)

Without an SPA there is no Elements provider tree. The pattern is simpler: the server creates the PaymentIntent while rendering the checkout page, injects the client secret into the template, and stripe-js confirms the card on form submit. Card details never touch your server.

## Server: create the intent, pass the secret to the template

Django example (Rails/Laravel are the same shape):

```
def checkout(request):
    intent = stripe.PaymentIntent.create(
        amount=2000,
        currency='usd',
        automatic_payment_methods={'enabled': True},
    )
    return render(request, 'checkout.html', {
        'client_secret': intent.client_secret,
        'publishable_key': PUBLISHABLE_KEY_VALUE,
    })
```

Set the API key from your environment at startup; never hardcode it.

## Template: mount the card, confirm on submit

```
[form id="payment-form"]
  [div id="card-element"][/div]
  [button]Pay[/button]
  [div id="card-errors"][/div]
[/form]
[script src="https://js.stripe.com/v3/"][/script]
[script]
  var stripe = Stripe('{{ publishable_key }}');
  var elements = stripe.elements();
  var card = elements.create('card');
  card.mount('#card-element');
  var form = document.getElementById('payment-form');
  form.addEventListener('submit', async function (e) {
    e.preventDefault();
    var result = await stripe.confirmCardPayment('{{ client_secret }}', {
      payment_method: { card: card },
    });
    if (result.error) {
      document.getElementById('card-errors').textContent = result.error.message;
    } else if (result.paymentIntent.status === 'succeeded') {
      window.location = '/order/complete';
    }
  });
[/script]
```

## Notes

- The client secret is single-use and tied to one intent; rendering it into the page is the intended design.
- Compute the amount server-side from your catalog, not from form input.
- Escape template variables per your framework's defaults so the secret renders as plain text, not HTML.