ezsite.ai › Blog › How to Integrate Billing and Stripe Checkout Into a Cloned Web App
← All articlesHow 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.

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 requirement | Checkout configuration | Application result |
|---|---|---|
| One-time product | mode: "payment" | Fulfill an order and grant the purchased entitlement |
| Recurring SaaS plan | mode: "subscription" | Store the Stripe customer and subscription, then synchronize access over time |
| Abandoned checkout cleanup | Handle checkout.session.expired | Release reservations or mark a pending purchase as expired |
| Subscription payment confirmation | Handle invoice.paid | Keep the subscription entitlement active after a successful recurring payment |
| Failed recurring payment | Handle invoice.payment_failed | Start 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
| Field | Purpose |
|---|---|
id | Internal billing record ID |
app_user_id | Authenticated application user ID |
stripe_customer_id | Stripe Customer ID |
stripe_subscription_id | Current Stripe Subscription ID |
stripe_price_id | Current Stripe Price ID |
subscription_status | Current Stripe subscription status |
current_period_end | Timestamp when the current paid period ends |
cancel_at_period_end | Whether cancellation is scheduled |
entitlement_tier | Application access tier such as free, starter, or pro |
entitlement_updated_at | Last entitlement synchronization time |
created_at | Record creation time |
updated_at | Last record update time |
orders
| Field | Purpose |
|---|---|
id | Internal order ID |
app_user_id | Owning application user |
stripe_checkout_session_id | Checkout Session ID |
stripe_payment_intent_id | PaymentIntent ID for one-time payments |
plan_key | Internal plan identifier |
amount_total | Amount recorded from the verified Stripe object |
currency | Currency recorded from Stripe |
status | pending, paid, expired, or refunded |
fulfilled_at | Time fulfillment completed |
created_at | Record creation time |
stripe_events
| Field | Purpose |
|---|---|
stripe_event_id | Globally unique Stripe event ID |
event_type | Event type such as checkout.session.completed |
status | received, processing, processed, or failed |
attempt_count | Number of processing attempts |
last_error | Most recent failure message |
processed_at | Successful processing time |
created_at | Event 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.
Should a cloned app use Stripe Payment Links or Checkout Sessions?
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.
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.
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, andcancel_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
2xxresponses, 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.