ezsite.ai › Blog › How to Add Stripe Payments When Converting a Site to a Fullstack App
← All articlesHow to Add Stripe Payments When Converting a Site to a Fullstack App
Key takeaways
- Create separate EZsite Edge Functions for checkout creation and Stripe webhooks.
- Keep Stripe secret keys and webhook secrets in protected server-side configuration.
- Use server-controlled product and price data instead of trusting client-supplied amounts.
- Redirect to the Checkout Session URL for hosted Checkout; use the required client-secret flow for embedded Checkout.
- Preserve the raw webhook body and verify the Stripe signature before parsing the event.
- Use event-specific handling for payments, asynchronous payments, refunds, disputes, invoices, and subscription changes.
- Store internal order and customer IDs alongside only the Stripe IDs needed for reconciliation and fulfillment.
- Make webhook processing, payment creation, and fulfillment idempotent and replay-safe.
- Grant digital access only from verified server-side payment or subscription state.

Add Stripe payments to a converted React or Vue site by keeping the product and checkout interface in the frontend, creating payment sessions in an EZsite Edge Function, and confirming payment through a signed Stripe webhook. Store internal order records and relevant Stripe IDs in your database, then grant access, fulfill orders, send email, or update CRM records only after verified webhook processing.
What is the implementation path in EZsite?
A practical EZsite implementation has four parts:
1. Frontend: Keep the cloned product cards, cart, pricing display, and checkout buttons in your React or Vue app.
2. Checkout Edge Function: In your EZsite project, create a server-side Edge Function such as create-checkout-session. This function validates the request, looks up server-approved products and prices, creates a Stripe Checkout Session, and returns the session URL or the data required by the selected Checkout integration.
3. Webhook Edge Function: Create a second Edge Function such as stripe-webhook. Expose it as a public HTTP endpoint so Stripe can send events to it. This function must verify the Stripe signature using the unmodified request body, process events idempotently, and update your database.
4. Secrets and data: Add Stripe secrets to the project’s protected server-side environment-variable or secrets settings—not to frontend code. Use the EZsite database, if enabled for the project, for orders, payments, subscriptions, webhook events, and customer relationships.
The exact names of EZsite dashboard controls can change. Look for the project areas labeled Edge Functions, Environment Variables, Secrets, Database, and Deploy/Publish. The important security boundary is that Stripe secret keys and webhook signing secrets are available only to server-side functions.
A typical request flow looks like this:
```text
Product page or cart
|
| POST /functions/v1/create-checkout-session
v
EZsite Edge Function
|
| Stripe secret-key API request
v
Stripe Checkout
|
| signed event POST
v
POST /functions/v1/stripe-webhook
|
v
EZsite database, fulfillment, access, email, or CRM workflow
```
If your EZsite project uses a different function URL format, use the deployed function URL shown in the EZsite dashboard. Do not expose a function by placing the Stripe secret key in the browser.
How do I convert a cloned product card or checkout button?
A cloned button is initially only a visual element. Convert it into a payment action by giving it a stable product reference and connecting its click handler to the checkout Edge Function.
1. Create a server-controlled product catalog
Do not let the browser choose an arbitrary amount or Stripe Price ID. Maintain a catalog in your database or in server-side configuration, for example:
```json
{
"starter-plan": {
"stripePriceId": "price_123",
"type": "one_time",
"active": true
},
"pro-membership": {
"stripePriceId": "price_456",
"type": "subscription",
"active": true
}
}
```
The frontend sends productId and quantity; the Edge Function looks up the corresponding Stripe Price ID and applies its own quantity, discount, shipping, and eligibility rules.
2. Add frontend button logic
For a hosted Checkout flow, the frontend can call the function and redirect the browser to the returned Session URL:
```js
async function startCheckout(productId, quantity = 1) {
const response = await fetch('/functions/v1/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
quantity,
returnPath: '/order/success'
})
});
if (!response.ok) {
throw new Error('Unable to start checkout');
}
const { url } = await response.json();
window.location.assign(url);
}
```
A product card might call startCheckout('starter-plan'). A cart should send a list of internal product IDs and quantities, not client-calculated totals:
```json
{
"items": [
{ "productId": "starter-plan", "quantity": 2 }
],
"returnPath": "/order/success"
}
```
The success page should use the returned order reference or Checkout Session ID to display status. It should not independently mark the order as paid.
3. Create the Checkout Session on the server
Illustrative Edge Function pseudocode:
```js
export default async function handler(request) {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, 405);
}
const user = await authenticateUser(request); // Optional but required for protected products
const input = await request.json();
const items = validateCart(input.items);
const catalogItems = await loadActiveCatalogItems(items);
const lineItems = buildStripeLineItems(catalogItems, items);
const order = await db.orders.insert({
user_id: user?.id ?? null,
status: 'pending',
currency: 'usd',
source: 'website'
});
const session = await stripe.checkout.sessions.create(
{
mode: determineMode(catalogItems), // 'payment' or 'subscription'
line_items: lineItems,
customer_email: user?.email,
client_reference_id: order.id,
metadata: {
order_id: order.id
},
success_url: ${APP_URL}/order/success?order_id=${order.id},
cancel_url: ${APP_URL}/cart
},
{
idempotencyKey: checkout:${order.id}
}
);
await db.orders.update(order.id, {
stripe_checkout_session_id: session.id
});
return json({
orderId: order.id,
sessionId: session.id,
url: session.url
});
}
```
For hosted Checkout, redirect the customer to session.url. For embedded Checkout, do not treat the URL as the embedded configuration. Use Stripe’s embedded Checkout initialization flow and return the appropriate client secret or other value required by that flow. The server still creates the Session, and the client still must not receive the Stripe secret key.
Which Stripe payment model should I use?
The correct model depends on what the converted site sells:
| Use case | Recommended Stripe setup | Fulfillment or access rule |
|---|---|---|
| One-time digital product | Checkout with mode: payment | Deliver only after a verified successful payment event. |
| Physical product | Checkout with mode: payment, shipping address, and applicable shipping or tax settings | Create a fulfillment task or order after payment confirmation. |
| Subscription or membership | Checkout with mode: subscription and Stripe Billing | Base access on the current subscription and invoice state. |
| Donation | Payment mode with a permitted amount or predefined donation prices | Record the donation after verified payment; do not assume every donation is recurring. |
| Gated content | Payment or subscription, depending on the offer | Link the paid order or subscription to the authenticated user and enforce access server-side. |
| Simple buy-now campaign | Payment Link | Use a webhook to connect the resulting Stripe object to your internal records when possible. |
For a cart containing both one-time and recurring items, use a product design that Stripe supports and that your fulfillment rules can represent clearly. Otherwise, split the purchase into separate transactions.
Where should Stripe keys and environment variables go?
Create protected server-side secrets in the EZsite project settings. Names such as these make the configuration easy to audit:
```text
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
APP_URL=https://your-domain.example
```
Only STRIPE_PUBLISHABLE_KEY may be used in browser code, and it is not a substitute for server authorization. STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET must be available only to Edge Functions.
Use test-mode credentials while developing. Before launch, configure live-mode secrets separately, verify the production return URLs, and register the production webhook endpoint in Stripe. Never commit secrets to the repository or place them in React, Vue, downloaded HTML, or client-side configuration that is shipped to users.
What database records should I store?
Use your internal IDs as the primary relationship keys and store Stripe IDs as external references. A small schema can include the following tables.
customers
iduser_idor account identifier, if users can sign instripe_customer_idemailcreated_at
orders
Use this table for a one-time purchase or a parent record for a checkout attempt.
iduser_idorcustomer_idstatus:pending,paid,partially_refunded,refunded,failed, orcancelledcurrencyamount_totalstripe_checkout_session_idstripe_payment_intent_id, for payment-mode Checkout when availablecreated_atpaid_at
order_items
idorder_idproduct_idstripe_price_idquantityunit_amountfulfillment_status
subscriptions
Use this table only for recurring billing.
iduser_idorcustomer_idstripe_subscription_idstripe_customer_idstripe_price_idstatuscurrent_period_startcurrent_period_endcancel_at_period_endcanceled_attrial_endupdated_at
stripe_events
stripe_event_idas a unique keyevent_typeobject_idreceived_atprocessed_atprocessing_statuserror_message
For a one-time payment, the important relationship is usually order → Checkout Session → PaymentIntent. For a subscription, use customer → subscription → invoices and PaymentIntents. Store only the identifiers your support, reconciliation, fulfillment, and access-control logic actually needs.
Why should a Stripe webhook confirm payment?
A success page is a customer-facing navigation result, not authoritative payment proof. A signed webhook should update the order and start fulfillment because the customer can close the tab, lose connectivity, revisit the URL, or reach the page before asynchronous processing finishes.
For an EZsite webhook Edge Function, preserve the raw request body before parsing JSON. Signature verification must receive the exact bytes or string Stripe sent:
```js
export default async function handler(request) {
const rawBody = await request.text();
const signature = request.headers.get('stripe-signature');
let event;
try {
event = stripe.webhooks.constructEvent(
rawBody,
signature,
env.STRIPE_WEBHOOK_SECRET
);
} catch (error) {
return new Response('Invalid signature', { status: 400 });
}
const alreadyProcessed = await db.stripe_events.exists(event.id);
if (alreadyProcessed) {
return new Response('Already processed', { status: 200 });
}
await db.stripe_events.insert({
stripe_event_id: event.id,
event_type: event.type,
object_id: event.data.object.id,
processing_status: 'processing'
});
try {
await processStripeEvent(event);
await db.stripe_events.markProcessed(event.id);
return new Response('ok', { status: 200 });
} catch (error) {
await db.stripe_events.markFailed(event.id, String(error));
return new Response('Retry later', { status: 500 });
}
}
```
Do not call request.json() before signature verification. If an EZsite runtime provides a framework-specific raw-body method, use that method and confirm that middleware has not altered the body.
Which Stripe events should I handle?
Handle events according to the payment model and your fulfillment requirements. Common event types include:
One-time payments
checkout.session.completed: records that Checkout completed and provides the Session context. Do not assume every asynchronous payment method has settled solely because Checkout completed.checkout.session.async_payment_succeeded: confirms a delayed payment method succeeded.checkout.session.async_payment_failed: records a delayed-payment failure.payment_intent.succeeded: confirms a successful PaymentIntent when your flow uses it as the payment source of truth.payment_intent.payment_failed: records a failed payment.charge.refunded: updates refund status and reverses or adjusts fulfillment where appropriate.charge.dispute.created: marks a disputed charge for review and can suspend fulfillment or access according to your business policy.
Subscriptions
checkout.session.completed: links the initial Checkout Session to the customer and subscription.customer.subscription.created: creates or activates the local subscription record.customer.subscription.updated: updates plan, status, trial, cancellation, and period fields.customer.subscription.deleted: revokes or limits access when the subscription ends.invoice.paid: records a successful renewal or invoice payment.invoice.payment_failed: starts failed-renewal handling and customer notifications.invoice.payment_action_required: tells you that the customer may need to authenticate or take another payment action.
Your exact event set should match the payment methods and Stripe products enabled for the account. Webhook handlers should be safe to run more than once and should retrieve the current Stripe object when the event payload does not contain enough information.
How do I handle subscriptions and access revocation?
Do not grant permanent access based only on the first subscription Checkout event. Store the Stripe subscription ID and update entitlement state when subscription and invoice events arrive.
Operational handling should include:
- Trials: store
trial_endand decide whether access is available during the trial. Handle trial ending and any required payment method separately. - Renewals: use
invoice.paidto record successful billing periods and extend access where appropriate. - Failed renewals: use
invoice.payment_failedandinvoice.payment_action_requiredto show a billing warning, notify the customer, or begin a grace period. - Plan changes: process
customer.subscription.updated, including price changes, quantity changes, prorations, and the effective billing period. - Cancellation: distinguish immediate cancellation from
cancel_at_period_end. Keep access until the period ends when that matches the subscription state and your policy. - Ended subscriptions: process
customer.subscription.deletedand revoke or limit access server-side. - Self-service: use Stripe’s customer portal or a controlled Edge Function for payment-method updates, invoices, plan changes, and cancellation. Confirm the user is authorized to act on the related customer record.
- Entitlements: check the database on protected requests. Hiding a button or gated page in React or Vue is not sufficient access control.
If your app grants digital access, make the entitlement record separate from the payment record. That lets you represent a refund, dispute, grace period, manual override, or subscription cancellation without losing the original transaction history.
How do I make the webhook and checkout flow secure?
At minimum:
- Keep secret keys server-side.
- Authenticate the user before creating a checkout session for an account-specific product or subscription.
- Validate the HTTP method, JSON shape, product IDs, quantities, return paths, and allowed currencies.
- Use an allowlist for redirect paths to prevent open redirects.
- Calculate prices and discounts from server-controlled data.
- Do not fulfill an order based solely on client-supplied metadata, a query parameter, or a success-page request.
- Verify
stripe-signatureagainst the raw body. - Store Stripe event IDs with a unique constraint for replay protection and idempotency.
- Use an idempotency key when creating a session or PaymentIntent from a retryable request.
- Add rate limiting to public checkout-session endpoints where the EZsite deployment supports it.
- Return quickly from webhook processing or move lengthy email, CRM, and fulfillment work to a queue or retryable job.
- Avoid logging full payment details, secret keys, or sensitive customer data.
- Restrict administrative and fulfillment endpoints with authorization checks.
Webhook endpoint authentication is normally provided by Stripe’s signature verification. Do not replace signature verification with an arbitrary shared URL token. If you add an additional endpoint control, treat it as defense in depth rather than as a substitute for verifying the Stripe signature.
Should I use Checkout, Payment Links, or Payment Element?
- Hosted Checkout: Usually the simplest option for a converted site. The server creates a Checkout Session and returns its
url; the browser redirects to that URL. - Embedded Checkout: Useful when Checkout should appear inside the app. The server creates the Session, and the frontend initializes Stripe’s embedded flow with the required client secret or configuration. Do not return or expose the Stripe secret key.
- Payment Links: Suitable for a fixed offer, campaign, email, or simple buy-now button when you do not need the app to build a custom cart at checkout.
- Payment Element: Appropriate when you need a highly customized payment interface. Create the PaymentIntent on the server, return only its client secret, and use webhook events to determine final payment status.
Payment Links can reduce implementation work, but they provide less control over a dynamic cart and account-specific authorization. Payment Element provides more UI control but requires more payment-state and error handling in your application.
How can EZsite workflows connect to payment events?
Treat database updates, email, fulfillment, CRM changes, and access grants as application actions triggered by verified payment state. The exact automation depends on the EZsite features enabled for your project.
A safe pattern is:
1. The webhook verifies the event.
2. The handler inserts or updates the order, payment, subscription, or refund record.
3. The handler creates a durable internal job or action record.
4. A worker, workflow, or follow-up Edge Function performs email, CRM, fulfillment, or access changes.
5. Each action records its own idempotency key and completion status.
For example, an internal fulfillment_jobs record might contain order_id, job_type, status, attempts, and completed_at. If your EZsite project does not provide queues or workflow automation, keep the webhook transaction short and call a separate retryable function rather than assuming a long-running webhook request will complete reliably.
What should I test before accepting live payments?
Test the entire lifecycle with Stripe test-mode credentials:
- Valid one-time checkout.
- Cancelled Checkout.
- Invalid product ID and quantity.
- Duplicate checkout requests.
- Delayed or asynchronous payment success.
- Failed payment.
- Refund.
- Dispute handling.
- Duplicate webhook delivery.
- Webhooks arriving out of order.
- Subscription trial, renewal, plan change, cancellation, and failed renewal.
- Customer self-service actions.
- Fulfillment, email, CRM, and gated-access updates.
- Unauthorized attempts to access another customer’s order.
- Success-page reloads and direct visits without a valid order.
Before launch, verify that:
- Every visible product maps to an active, server-approved Stripe Price ID.
- Test and live secrets are separated.
- The production webhook endpoint is registered in Stripe.
- Signature verification uses the raw request body.
- Duplicate events cannot duplicate fulfillment, email, or access grants.
- The success page reads order status but cannot mark an order paid.
- Return URLs, domain configuration, taxes, shipping, receipts, and customer emails match the business requirements.
- Protected content checks entitlement on the server.
Frequently asked questions
Can I add Stripe to a website cloned into React or Vue?
Yes. Keep the cloned interface, then add a checkout Edge Function, protected server-side Stripe secrets, a signed webhook Edge Function, and persistent order or subscription records.
Where do I create the Stripe endpoints in EZsite?
Create one Edge Function for checkout-session or PaymentIntent creation and another Edge Function for Stripe webhooks. Deploy both from the EZsite project, expose the webhook function at a public HTTPS endpoint, and register that endpoint in Stripe.
Where should I store Stripe secret keys in EZsite?
Store STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in EZsite’s protected server-side environment-variable or secrets settings. Never place them in React, Vue, browser JavaScript, or downloaded site source.
What does the checkout endpoint return?
For hosted Checkout, return the Checkout Session url and redirect the browser to it. For embedded Checkout, return the client secret or other value required by Stripe’s embedded initialization flow; do not describe the hosted Session URL as embedded configuration.
Can the success page confirm that payment succeeded?
No. The success page can display order status, but a verified Stripe webhook should confirm payment and trigger fulfillment or access changes.
How do I verify a Stripe webhook in an EZsite Edge Function?
Read the raw request body first, obtain the stripe-signature header, and pass both to Stripe’s webhook-signature verification method with STRIPE_WEBHOOK_SECRET. Parse the event only after verification succeeds.
Do I need Stripe Billing for subscriptions?
Yes. Use Stripe Billing and subscription webhooks for recurring plans, trials, renewals, prorations, payment recovery, cancellation, and access revocation.
How do I prevent duplicate orders and fulfillment?
Use an internal order ID, Stripe idempotency keys for create operations, a unique stripe_event_id record for webhook replay protection, and idempotent fulfillment jobs. Do not assume Stripe delivers events only once or in order.
Can I use a Stripe Payment Link for a cloned button?
Yes, for a fixed offer or simple campaign. Use a Payment Link when the app does not need to calculate a dynamic cart or enforce complex account-specific rules. Use a server-created Checkout Session when those controls matter.
How should gated content be secured?
Grant access only after verified payment or subscription state is stored for the authenticated user. Enforce entitlement on server-side requests; hiding content or buttons in the frontend is not sufficient.
Key implementation takeaways
- Use the cloned React or Vue interface for presentation, not for secret-key payment operations.
- Create separate EZsite Edge Functions for checkout creation and Stripe webhooks.
- Store Stripe secrets in protected server-side configuration.
- Validate products, quantities, discounts, and authorization on the server.
- Use the Checkout Session URL for hosted Checkout and the appropriate client secret flow for embedded Checkout.
- Preserve the raw webhook body and verify the Stripe signature before parsing or processing the event.
- Model one-time orders, payments, subscriptions, refunds, disputes, and entitlements separately when their business behavior differs.
- Make webhook processing and fulfillment idempotent, replay-safe, and tolerant of out-of-order events.
- Let webhook-confirmed state—not the success page or client metadata—control fulfillment and access.
References
- https://ezsite.ai`**
- https://ezsite.ai
- https://docs.stripe.com/payments/checkout/quickstarts
- https://ezsite.ai/guides/user/getting-started/edge-function
- https://docs.stripe.com/keys-best-practices
- https://docs.stripe.com/keys
FAQ
Can I add Stripe to a website cloned into React or Vue?
Yes. Keep the cloned interface, then add a checkout Edge Function, protected server-side Stripe secrets, a signed webhook Edge Function, and persistent order or subscription records.
Where do I create the Stripe endpoints in EZsite?
Create one Edge Function for checkout-session or PaymentIntent creation and another Edge Function for Stripe webhooks. Deploy both from the EZsite project, expose the webhook function at a public HTTPS endpoint, and register that endpoint in Stripe.
Where should I store Stripe secret keys in EZsite?
Store `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` in EZsite’s protected server-side environment-variable or secrets settings. Never place them in frontend code.
What does the checkout endpoint return?
Hosted Checkout returns the Checkout Session `url`, which the browser opens or redirects to. Embedded Checkout uses the client secret or other value required by Stripe’s embedded initialization flow.
How do I verify a Stripe webhook in an EZsite Edge Function?
Read the raw request body before parsing JSON, obtain the `stripe-signature` header, and verify both with Stripe’s webhook-signature method and `STRIPE_WEBHOOK_SECRET`.
Do I need Stripe Billing for subscriptions?
Yes. Use Stripe Billing and subscription webhooks for recurring plans, trials, renewals, prorations, failed payments, customer self-service, cancellation, and access revocation.
How do I prevent duplicate Stripe orders and fulfillment?
Use internal order IDs, Stripe idempotency keys, unique Stripe event IDs, and idempotent fulfillment jobs. Webhook handlers must tolerate retries and out-of-order delivery.
Can a success page confirm payment?
No. A success page is for customer experience. A verified Stripe webhook should confirm payment and control fulfillment or access changes.