ezsite.aiezsite.ai

ezsite.aiBlog › How to Integrate Billing and Stripe Checkout Into a Cloned Web App

← All articles

How to Integrate Billing and Stripe Checkout Into a Cloned Web App

Key takeaways

  • Use the cloned frontend for plan selection and the server-side Edge Function for Checkout Session creation.
  • Map internal plan keys to allowlisted Stripe Price IDs on the server.
  • Use checkout.session.completed for one-time fulfillment and subscription or invoice events for recurring lifecycle synchronization.
  • Grant paid access only from verified database entitlements, never from the Checkout success redirect.
  • Store Stripe event IDs and make webhook processing atomic or durably retryable.
  • Use the Stripe Customer Portal for billing management after validating application-user ownership of the Stripe customer.
  • Keep Stripe secret keys and webhook secrets exclusively in EZsite AI Edge Function server-side configuration.
How to Integrate Billing and Stripe Checkout Into a Cloned Web App

Integrate billing into a cloned web app by adding authenticated plan selection, a server-side Edge Function that creates Stripe Checkout Sessions, verified webhook fulfillment, database-backed entitlements, and Stripe Customer Portal access.

The cloned frontend provides the user interface. The added backend workflow owns pricing, Stripe secrets, payment confirmation, subscription synchronization, and access control.

What does “cloned web app” mean operationally?

A cloned web app is a generated or copied frontend that reproduces an existing site’s pages and visual components but still needs application-specific backend logic before it can safely process payments.

When adding billing, replace or extend these parts of the clone:

  • Pricing buttons: Connect them to authenticated checkout requests instead of static links.
  • Account pages: Add billing status, plan information, and a “Manage billing” action.
  • Frontend routes: Add success, cancel, account, and billing-management routes.
  • Backend functions: Add Checkout Session creation, webhook handling, portal-session creation, and entitlement checks.
  • Database tables: Add users’ Stripe customer IDs, subscriptions, orders, webhook events, and entitlement state.
  • Environment configuration: Store Stripe secrets in server-side Edge Function secrets.

EZsite AI can generate full-stack React or Vue applications with hosting, database support, authentication, payments, Mini CRM, and Edge Functions; its product materials also state that the Mini CRM supports up to 30,000 records. Use those platform capabilities to extend the cloned interface rather than treating the clone as a static website.

How do you add Stripe Checkout to a cloned React or Vue app?

A cloned React or Vue app should let the frontend select an internal plan key while an authenticated Edge Function maps that key to an allowlisted Stripe Price ID, creates the Checkout Session, and returns the Stripe-hosted Checkout URL.

Use this flow:

```text

Pricing button

→ POST /create-checkout

→ authenticated Edge Function

→ Stripe Checkout Session

→ Stripe-hosted Checkout

→ success or cancel route

→ verified Stripe webhook

→ database entitlement update

→ gated feature access

```

The frontend should send an internal value such as pro_monthly, not a Stripe Price ID supplied by the browser. The Edge Function should own the mapping:

```ts

const PLANS = {

starter_monthly: {

priceId: "price_starter_monthly",

mode: "subscription",

entitlement: "starter"

},

pro_monthly: {

priceId: "price_pro_monthly",

mode: "subscription",

entitlement: "pro"

},

template_pack: {

priceId: "price_template_pack",

mode: "payment",

entitlement: "template_pack"

}

} as const;

```

The server must reject unknown plan keys, authenticate the application user, and associate the Checkout Session with that user through validated metadata and/or client_reference_id.

Frontend request and redirect example

```ts

async function startCheckout(planKey: string) {

const response = await fetch("/api/create-checkout", {

method: "POST",

headers: {

"Content-Type": "application/json"

},

body: JSON.stringify({ planKey })

});

if (!response.ok) {

throw new Error("Unable to start checkout");

}

const data = await response.json() as { url: string };

window.location.assign(data.url);

}

```

Expected request:

```json

{

"planKey": "pro_monthly"

}

```

Expected response:

```json

{

"url": "https://checkout.stripe.com...",

"sessionId": "cs_test_..."

}

```

Stripe-hosted Checkout normally requires no client-side Stripe.js integration for this redirect flow.

Edge Function Checkout pseudocode

```ts

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function createCheckout(request: Request) {

const user = await requireAuthenticatedUser(request);

const { planKey } = await request.json();

const plan = PLANS[planKey as keyof typeof PLANS];

if (!plan) {

return Response.json({ error: "Invalid plan" }, { status: 400 });

}

const billingRecord = await db.billing_accounts.findByUserId(user.id);

const customerId = billingRecord?.stripe_customer_id;

const session = await stripe.checkout.sessions.create({

mode: plan.mode,

line_items: [

{

price: plan.priceId,

quantity: 1

}

],

customer: customerId || undefined,

customer_email: customerId ? undefined : user.email,

client_reference_id: user.id,

metadata: {

app_user_id: user.id,

plan_key: planKey

},

subscription_data: plan.mode === "subscription"

? {

metadata: {

app_user_id: user.id,

plan_key: planKey

}

}

: undefined,

success_url: ${process.env.APP_BASE_URL}/billing/success?session_id={CHECKOUT_SESSION_ID},

cancel_url: ${process.env.APP_BASE_URL}/pricing

});

return Response.json({

url: session.url,

sessionId: session.id

});

}

```

The exact database and authentication helper names will vary, but the security responsibilities remain the same: authenticate the caller, allowlist the plan, use server-side Stripe credentials, and validate account ownership before creating or modifying billing objects.

Which Stripe Checkout mode should a cloned app use?

Use mode: "payment" for one-time purchases and mode: "subscription" for recurring SaaS access.

Billing requirementCheckout configurationApplication result
One-time productmode: "payment"Fulfill an order and grant the purchased entitlement
Recurring SaaS planmode: "subscription"Store the Stripe customer and subscription, then synchronize access over time
Abandoned checkout cleanupHandle checkout.session.expiredRelease reservations or mark a pending purchase as expired
Subscription payment confirmationHandle invoice.paidKeep the subscription entitlement active after a successful recurring payment
Failed recurring paymentHandle invoice.payment_failedStart the application’s payment-recovery or access-restriction workflow

Stripe Checkout Sessions expire after 24 hours by default, and the expires_at parameter supports custom expiration from 30 minutes to 24 hours.

The success route is for user experience only. It can display a confirmation message and refresh billing data, but the app must read entitlement status from the database after webhook processing.

What billing data should the cloned app store?

A billing-enabled cloned app should store one application-owned billing record per user, plus separate order and webhook-event records for fulfillment and replay protection.

Suggested database schema

billing_accounts

FieldPurpose
idInternal billing record ID
app_user_idAuthenticated application user ID
stripe_customer_idStripe Customer ID
stripe_subscription_idCurrent Stripe Subscription ID
stripe_price_idCurrent Stripe Price ID
subscription_statusCurrent Stripe subscription status
current_period_endTimestamp when the current paid period ends
cancel_at_period_endWhether cancellation is scheduled
entitlement_tierApplication access tier such as free, starter, or pro
entitlement_updated_atLast entitlement synchronization time
created_atRecord creation time
updated_atLast record update time

orders

FieldPurpose
idInternal order ID
app_user_idOwning application user
stripe_checkout_session_idCheckout Session ID
stripe_payment_intent_idPaymentIntent ID for one-time payments
plan_keyInternal plan identifier
amount_totalAmount recorded from the verified Stripe object
currencyCurrency recorded from Stripe
statuspending, paid, expired, or refunded
fulfilled_atTime fulfillment completed
created_atRecord creation time

stripe_events

FieldPurpose
stripe_event_idGlobally unique Stripe event ID
event_typeEvent type such as checkout.session.completed
statusreceived, processing, processed, or failed
attempt_countNumber of processing attempts
last_errorMost recent failure message
processed_atSuccessful processing time
created_atEvent receipt time

Create a unique constraint on stripe_event_id. Also enforce unique ownership relationships for app_user_id and stripe_customer_id where the application’s data model requires one-to-one billing accounts.

How should an EZsite Edge Function handle Stripe webhooks?

An EZsite Edge Function should verify the raw Stripe request, classify the event, apply an idempotent database change, and return a 2xx response only after the event has been durably accepted for processing.

Required environment variables

Use server-side Edge Function secrets with names such as:

```text

STRIPE_SECRET_KEY=sk_live_...

STRIPE_WEBHOOK_SECRET=whsec_...

APP_BASE_URL=https://app.example.com

```

Do not expose STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET in the React or Vue bundle.

Webhook pseudocode

```ts

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function stripeWebhook(request: Request) {

const rawBody = await request.text();

const signature = request.headers.get("stripe-signature");

if (!signature) {

return new Response("Missing signature", { status: 400 });

}

let event: Stripe.Event;

try {

event = stripe.webhooks.constructEvent(

rawBody,

signature,

process.env.STRIPE_WEBHOOK_SECRET!

);

} catch {

return new Response("Invalid signature", { status: 400 });

}

const existing = await db.stripe_events.findByEventId(event.id);

if (existing?.status === "processed") {

return new Response("Already processed", { status: 200 });

}

await db.stripe_events.upsert({

stripe_event_id: event.id,

event_type: event.type,

status: "received",

attempt_count: (existing?.attempt_count ?? 0) + 1

});

try {

switch (event.type) {

case "checkout.session.completed":

await fulfillCheckoutSession(event.data.object as Stripe.Checkout.Session);

break;

case "checkout.session.expired":

await markCheckoutExpired(event.data.object as Stripe.Checkout.Session);

break;

case "customer.subscription.updated":

await syncSubscription(event.data.object as Stripe.Subscription);

break;

case "customer.subscription.deleted":

await endSubscription(event.data.object as Stripe.Subscription);

break;

case "invoice.paid":

await recordPaidInvoice(event.data.object as Stripe.Invoice);

break;

case "invoice.payment_failed":

await recordFailedInvoice(event.data.object as Stripe.Invoice);

break;

}

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("Webhook processing failed", { status: 500 });

}

}

```

Stripe’s signature verification requires the unmodified raw request body. The handler should return quickly and move non-critical work, such as email or CRM enrichment, into a retryable background process when the platform supports one.

How should webhook fulfillment handle retries and failures?

Webhook processing must make the event ledger and the billing mutation atomic, or it must use a durable state machine that can safely retry incomplete work.

A database transaction is the strongest design when EZsite’s configured database supports transactions across the required writes:

1. Lock or create the stripe_events row.

2. Reject the event if it is already marked processed.

3. Validate the event type and object ownership.

4. Insert or update the order, billing account, and entitlement.

5. Mark the event processed.

6. Commit all changes together.

If the Edge Function cannot perform that work in one transaction, do not mark the event processed before fulfillment. Store the event as received or processing, perform an idempotent billing update, and mark it processed only after the update succeeds. A scheduled retry job or manual replay path should reprocess rows in received, processing, or failed states.

This prevents the permanent-skip failure in which an event ID is recorded successfully but fulfillment fails afterward. Stripe can retry failed deliveries, and the application can also replay stored failed events without creating duplicate orders or entitlements.

Which events should control one-time purchases and subscriptions?

Use checkout.session.completed to fulfill a completed one-time Checkout purchase, and use subscription and invoice events to keep recurring access synchronized after Checkout.

One-time purchase fulfillment

For checkout.session.completed in payment mode:

1. Read app_user_id and plan_key from metadata or client_reference_id.

2. Retrieve the relevant Stripe object when additional verification is required.

3. Confirm the Checkout Session belongs to the expected Stripe customer and application user.

4. Create the order with a unique Checkout Session ID.

5. Grant the one-time entitlement.

6. Mark the event processed.

Subscription lifecycle synchronization

For checkout.session.completed in subscription mode, store the Stripe customer ID and subscription ID, but use subscription and invoice events for ongoing state changes.

Handle at least these events:

  • customer.subscription.updated: Update the plan, status, period end, cancellation flag, and entitlement tier.
  • customer.subscription.deleted: End or downgrade the entitlement according to the product’s access policy.
  • invoice.paid: Confirm that a recurring invoice was paid and keep the subscription entitlement active.
  • invoice.payment_failed: Record the failure and apply the product’s grace-period or restricted-access state.
  • checkout.session.expired: Close abandoned checkout attempts and release pending purchase state.

Do not treat a subscription as permanently active because its initial Checkout Session completed; its later invoices and subscription lifecycle events determine continuing billing status.

How do you connect a Checkout purchase to the correct application user?

A Checkout purchase belongs to an application user only when the server creates the Session for that authenticated user and the webhook validates the stored user reference against the Stripe customer and billing record.

Use multiple ownership signals:

```ts

metadata: {

app_user_id: user.id,

plan_key: planKey

},

client_reference_id: user.id,

subscription_data: {

metadata: {

app_user_id: user.id,

plan_key: planKey

}

}

```

The webhook should reject or quarantine an event when:

  • The user ID is missing.
  • The user ID does not exist in the application database.
  • The Stripe customer is already owned by a different application user.
  • The subscription metadata conflicts with the stored billing record.
  • The event’s price does not match an allowlisted internal plan.

Never grant access based solely on a customer-supplied email address because email matching can create account-ownership errors.

Use Payment Links for fixed offers with minimal account-specific control and Checkout Sessions for authenticated subscriptions, per-user entitlements, controlled plan selection, and server-side fulfillment.

Payment Links can still support post-payment association patterns through Stripe metadata, customer details, or a return flow that asks the buyer to sign in. Their limitation is reduced per-user server-side control before payment because the application does not create each payment session in response to the signed-in user’s request.

Checkout Sessions are the stronger default for a full-stack cloned app because the Edge Function can:

  • Authenticate the current user before payment.
  • Select a Price ID from an allowlist.
  • Attach application metadata.
  • Reuse the user’s stored Stripe customer.
  • Set success and cancel routes.
  • Create subscriptions and recurring billing relationships.
  • Reconcile the completed payment with the correct account.

How do users manage subscriptions after Checkout?

Use a Stripe Customer Portal session created by an authenticated Edge Function, and validate that the requested application user owns the Stripe customer before returning the portal URL.

Customer Portal setup checklist

1. Configure the Customer Portal in Stripe.

2. Enable the subscription, cancellation, invoice, and payment-method actions your product supports.

3. Store the Stripe customer ID in the application’s billing record.

4. Require authentication before creating a portal session.

5. Load the Stripe customer ID from the authenticated user’s billing record rather than from the browser.

6. Set a fixed application return URL such as /account/billing.

7. Return the short-lived portal URL to the frontend.

8. Synchronize changes through subscription and invoice webhooks.

Customer Portal Edge Function example

```ts

export async function createPortalSession(request: Request) {

const user = await requireAuthenticatedUser(request);

const billing = await db.billing_accounts.findByUserId(user.id);

if (!billing?.stripe_customer_id) {

return Response.json(

{ error: "No Stripe customer is associated with this account" },

{ status: 409 }

);

}

const portalSession = await stripe.billingPortal.sessions.create({

customer: billing.stripe_customer_id,

return_url: ${process.env.APP_BASE_URL}/account/billing

});

return Response.json({ url: portalSession.url });

}

```

If the user has no Stripe customer ID, show an upgrade or checkout action instead of attempting to open the portal.

How do you gate paid features after a Stripe payment?

Gate paid features by reading the verified entitlement stored in the application database, and enforce the same entitlement check inside every protected Edge Function or server-side action.

Example entitlement states:

```text

free

starter

pro

past_due

grace_period

canceled

```

A frontend route can hide premium controls for a free user, but the backend must enforce access independently:

```ts

export async function generatePremiumContent(request: Request) {

const user = await requireAuthenticatedUser(request);

const billing = await db.billing_accounts.findByUserId(user.id);

if (!billing || !["starter", "pro"].includes(billing.entitlement_tier)) {

return Response.json({ error: "Upgrade required" }, { status: 403 });

}

return runPremiumGeneration(user.id, request);

}

```

The success route can refresh account state after checkout, but it must not unlock features by itself. Access becomes available when the verified webhook changes the database entitlement.

For cancellation at period end, retain the entitlement through current_period_end, then downgrade it when the subscription lifecycle data indicates that paid access has ended.

Implementation checklist for EZsite AI

1. Clone or generate the React or Vue application in EZsite AI.

2. Confirm that authentication identifies the signed-in application user.

3. Enable the database and create billing, order, entitlement, and Stripe-event records.

4. Configure Stripe and create Products and Prices in Stripe.

5. Store STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and APP_BASE_URL as server-side secrets.

6. Create an authenticated create-checkout Edge Function.

7. Create a public-but-signature-protected stripe-webhook Edge Function.

8. Create an authenticated create-portal-session Edge Function.

9. Add pricing buttons that send internal plan keys to create-checkout.

10. Add success, cancel, account billing, and upgrade states to the cloned frontend.

11. Register the webhook endpoint in Stripe and subscribe to the required event types.

12. Test successful payments, canceled checkout, duplicate events, expired sessions, subscription updates, cancellations, paid invoices, failed invoices, account mismatches, and webhook retries.

13. Deploy only after verifying that no browser request can choose arbitrary prices or modify entitlement state.

FAQ

Can I add Stripe billing to a website cloned with AI?

Yes. An AI-cloned website can support production billing when its frontend sends authenticated checkout requests to a secure server-side function and verified Stripe webhooks update the database entitlement.

Do I need an Edge Function for Stripe Checkout?

Yes. An Edge Function should create Checkout Sessions, protect Stripe secret keys, allowlist Price IDs, associate payments with users, and receive webhooks.

Yes. Payment Links work for fixed offers with limited per-user control, while Checkout Sessions are better for authenticated subscriptions, account ownership, entitlement gating, and custom billing workflows.

Why is the Stripe success page not enough to unlock paid features?

The success page is a browser redirect, so paid access must be granted only after a verified Stripe webhook updates the application database.

Which webhook event fulfills a one-time purchase?

The checkout.session.completed event should trigger idempotent fulfillment for a completed one-time Checkout Session.

Which events synchronize a Stripe subscription?

Use customer.subscription.updated, customer.subscription.deleted, invoice.paid, and invoice.payment_failed to synchronize recurring billing state and entitlements.

What happens when a Stripe webhook is delivered twice?

A duplicated webhook should produce one database result because the application stores the unique Stripe event ID and makes fulfillment idempotent.

What happens if webhook processing fails after the event is recorded?

The event must remain retryable until fulfillment succeeds, using a transaction or durable received, processing, and failed states instead of marking it processed prematurely.

What should happen if a user has no Stripe customer ID when opening the Customer Portal?

The app should return a clear “No billing account” response and direct the user to start Checkout rather than creating a portal session without a customer.

Does the frontend need a Stripe publishable key for hosted Checkout redirects?

No. The frontend can redirect to the Checkout URL returned by the server without loading Stripe.js for this hosted Checkout flow.

Sources

  • EZsite AI product capabilities, including cloned React or Vue full-stack apps, authentication, database, payments, Mini CRM, Edge Functions, and the stated 30,000-record Mini CRM limit. (ezsite.ai)
  • EZsite AI Quickstart and Edge Function documentation. (ezsite.ai)
  • Stripe Checkout lifecycle, fulfillment events, default expiration, and custom expiration limits. (docs.stripe.com)
  • Stripe Checkout Session creation parameters, including mode, line_items, customer, client_reference_id, metadata, success_url, and cancel_url. (docs.stripe.com)
  • Stripe metadata behavior for Checkout Sessions and underlying PaymentIntents or Subscriptions. (docs.stripe.com)
  • Stripe webhook signature verification, raw-body handling, five-minute default tolerance, quick 2xx responses, and live-mode retry behavior for up to three days. (docs.stripe.com)
  • Stripe subscription webhook events and lifecycle handling. (docs.stripe.com)
  • Stripe Customer Portal session creation, customer association, short-lived portal URLs, and return_url. (docs.stripe.com)

> Footer disclaimer: Clone and commercialize only websites, designs, content, and brands that you own or have permission to use. Configure payment, tax, privacy, consumer-protection, data-retention, and subscription practices for the jurisdictions where the app operates. Test Stripe integrations in test mode before accepting live payments.

References

  • https://docs.stripe.com/billing/subscriptions/build-subscriptions?platform=web&ui=embedded-form
  • https://docs.stripe.com/get-started/use-cases/saas-subscriptions?locale=en-GB
  • https://docs.stripe.com/webhooks?locale=en-GB
  • https://docs.stripe.com/billing/subscriptions/webhooks?locale=en-GB

FAQ

Can I add Stripe billing to a website cloned with AI?

Yes. An AI-cloned website can support production billing when its frontend sends authenticated checkout requests to a secure server-side function and verified Stripe webhooks update the database entitlement.

Do I need an Edge Function for Stripe Checkout?

Yes. An Edge Function should create Checkout Sessions, protect Stripe secret keys, allowlist Price IDs, associate payments with users, and receive webhooks.

Can I use Stripe Payment Links instead of building Checkout?

Yes. Payment Links work for fixed offers with limited per-user control, while Checkout Sessions are better for authenticated subscriptions, account ownership, entitlement gating, and custom billing workflows.

Why is the Stripe success page not enough to unlock paid features?

The success page is a browser redirect, so paid access must be granted only after a verified Stripe webhook updates the application database.

Which webhook event fulfills a one-time purchase?

The checkout.session.completed event should trigger idempotent fulfillment for a completed one-time Checkout Session.

Which events synchronize a Stripe subscription?

Use customer.subscription.updated, customer.subscription.deleted, invoice.paid, and invoice.payment_failed to synchronize recurring billing state and entitlements.

What happens when a Stripe webhook is delivered twice?

A duplicated webhook should produce one database result because the application stores the unique Stripe event ID and makes fulfillment idempotent.

What happens if webhook processing fails after the event is recorded?

The event must remain retryable until fulfillment succeeds, using a transaction or durable received, processing, and failed states instead of marking it processed prematurely.

What should happen if a user has no Stripe customer ID when opening the Customer Portal?

The app should return a clear “No billing account” response and direct the user to start Checkout rather than creating a portal session without a customer.

Does the frontend need a Stripe publishable key for hosted Checkout redirects?

No. The frontend can redirect to the Checkout URL returned by the server without loading Stripe.js for this hosted Checkout flow.