# Integrating Stripe Payment Systems into a Vibe-Coded SaaS Application

> Easily integrate Stripe payments into your Vibe-coded SaaS. Learn to use server-side pricing, Stripe Checkout Sessions, and Supabase webhooks for seamless subscription management.

- Repository: [Datawhale/easy-vibe](https://github.com/datawhalechina/easy-vibe)
- Tags: how-to-guide
- Published: 2026-05-10

---

**Stripe payment integration in a vibe-coded SaaS application requires server-side price determination, Stripe Checkout Sessions for secure hosted payments, and webhook handlers to synchronize subscription state with Supabase.**

The `datawhalechina/easy-vibe` repository provides a full-stack documentation system for building SaaS products with Supabase and AI-assisted code generation. Integrating Stripe into this architecture follows a four-layer pattern documented in [`docs/zh-cn/stage-2/backend/stripe-payment/index.md`](https://github.com/datawhalechina/easy-vibe/blob/main/docs/zh-cn/stage-2/backend/stripe-payment/index.md), ensuring secure transaction handling and reliable subscription management.

## Architecture Overview

The Stripe integration spans four distinct layers, each with specific responsibilities regarding payment processing.

**Front-end Layer** handles UI rendering, purchase initiation, and status polling. According to the source documentation, the frontend "shows buttons, initiates purchases, and redirects pages"【/cache/.../stripe-payment/index.md†L61-L64】. It calls backend endpoints to create Checkout Sessions and redirects users to Stripe's hosted payment page.

**Back-end API Layer** determines pricing, creates Checkout Sessions, validates webhook signatures, and updates the database. The backend reads price IDs from environment variables (`STRIPE_PRICE_PRO_MONTHLY`, `STRIPE_PRICE_PRO_YEARLY`) and calls Stripe's `checkout.sessions.create` API【/cache/.../stripe-payment/index.md†L38-L43】.

**Database Layer (Supabase)** persists user records, subscription states, and order history. It stores the Stripe `customer_id`, `subscription_id`, and payment status values including `paid`, `past_due`, and `canceled`【/cache/.../stripe-payment/index.md†L61-L65】.

**Stripe Layer** provides the hosted checkout UI, subscription management portal, and webhook delivery. Products and Prices are defined in the Stripe Dashboard, while Checkout Sessions handle the payment flow securely【/cache/.../stripe-payment/index.md†L29-L36】.

## Core Payment Flow

The integration follows a six-step sequence that ensures security by never trusting client-side price data:

1. **User initiates upgrade** — The frontend sends a `POST` request to `/api/billing/create-checkout-session` containing only the plan identifier (e.g., "monthly" or "yearly"), never the price amount.

2. **Backend validates and creates session** — The API looks up the corresponding `price_id` from environment variables, creates a Stripe Checkout Session, and returns the `session.url`.

3. **Redirect to Stripe** — The frontend redirects the browser to the Stripe-hosted checkout page.

4. **Payment processing** — Stripe handles the payment collection and security compliance.

5. **Webhook confirmation** — Upon completion, Stripe sends a `checkout.session.completed` webhook to the backend endpoint.

6. **Database update** — The backend verifies the webhook signature using `STRIPE_WEBHOOK_SECRET`, updates the user's subscription record in Supabase, and returns HTTP 200.

**Critical security principle:** As documented in the repository, "the success page does not equal payment success" (成功页面不等于支付成功)【/cache/.../stripe-payment/index.md†L43-L49】. Always rely on webhook confirmation rather than the `success_url` callback alone.

## Environment Configuration

Configure the following variables in your `.env` file, ensuring these values are never committed to version control【/cache/.../appendix/2-development-tools/environment-path.md†L110-L118】:

- **`STRIPE_SECRET_KEY`** — Server-side secret for authenticating Stripe API calls. Obtain from Stripe Dashboard → API keys【/cache/.../stripe-payment/index.md†L49-L50】.
- **`STRIPE_WEBHOOK_SECRET`** — Secret for verifying webhook signatures. Obtain from Stripe Dashboard → Webhook settings【/cache/.../stripe-payment/index.md†L51-L52】.
- **`STRIPE_PRICE_PRO_MONTHLY`** — The `price_id` for your monthly plan, created in the Stripe Dashboard【/cache/.../stripe-payment/index.md†L94-L100】.
- **`STRIPE_PRICE_PRO_YEARLY`** — The `price_id` for your yearly plan.
- **`APP_URL`** — Your frontend base URL (e.g., `https://myapp.vercel.app`).
- **`SUPABASE_URL`** and **`SUPABASE_SERVICE_ROLE_KEY`** — Existing Supabase connection credentials.

## Implementation Guide

### Creating Checkout Sessions

Implement the backend endpoint that creates Stripe Checkout Sessions. This code belongs in your API routes directory (e.g., [`src/api/billing.js`](https://github.com/datawhalechina/easy-vibe/blob/main/src/api/billing.js)):

```javascript
import Stripe from "stripe";
import dotenv from "dotenv";
dotenv.config();

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2023-10-16",
});

export async function createCheckoutSession(req, res) {
  const { plan } = req.body; // "monthly" or "yearly"
  const priceId =
    plan === "monthly"
      ? process.env.STRIPE_PRICE_PRO_MONTHLY
      : process.env.STRIPE_PRICE_PRO_YEARLY;

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ["card"],
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/cancel`,
  });

  res.json({ url: session.url });
}

```

The price must be determined server-side from environment variables, satisfying the security requirement that the client never sends price amounts【/cache/.../stripe-payment/index.md†L31-L35】.

### Handling Stripe Webhooks

Create a webhook endpoint to process Stripe events and update Supabase. Store this in [`src/api/webhook.js`](https://github.com/datawhalechina/easy-vibe/blob/main/src/api/webhook.js):

```javascript
import Stripe from "stripe";
import * as supabase from "@supabase/supabase-js";
import dotenv from "dotenv";
dotenv.config();

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2023-10-16",
});

export async function stripeWebhook(req, res) {
  const sig = req.headers["stripe-signature"];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.rawBody,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error("Webhook signature verification failed.", err);
    return res.sendStatus(400);
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    const supabaseClient = supabase.createClient(
      process.env.SUPABASE_URL,
      process.env.SUPABASE_SERVICE_ROLE_KEY
    );

    const { user_id } = session.metadata;

    await supabaseClient
      .from("subscriptions")
      .upsert({
        user_id,
        stripe_customer_id: session.customer,
        stripe_subscription_id: session.subscription,
        status: "active",
        current_period_end: new Date(
          session.subscription?.current_period_end * 1000
        ),
      });
  }

  res.sendStatus(200);
}

```

The raw request body is required for signature verification. This handler updates a `subscriptions` table that tracks the user's payment state.

### Frontend Integration

Implement a Vue 3 component to trigger the checkout flow. This can be adapted for VitePress themes in [`docs/.vitepress/theme/components/BuyButton.vue`](https://github.com/datawhalechina/easy-vibe/blob/main/docs/.vitepress/theme/components/BuyButton.vue):

```vue
<script setup>
import { ref } from "vue";

const loading = ref(false);
async function startCheckout(plan) {
  loading.value = true;
  const resp = await fetch("/api/billing/create-checkout-session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ plan }),
  });
  const { url } = await resp.json();
  window.location.href = url;
  loading.value = false;
}
</script>

<template>
  <button @click="startCheckout('monthly')" :disabled="loading">
    {{ loading ? "Redirecting…" : "Buy Monthly" }}
  </button>
  <button @click="startCheckout('yearly')" :disabled="loading">
    {{ loading ? "Redirecting…" : "Buy Yearly" }}
  </button>
</template>

```

For querying payment status, implement a status check function:

```javascript
// src/api/user-status.js
export async function getUserStatus(userId) {
  const resp = await fetch(`/api/user/status?uid=${userId}`);
  const data = await resp.json();
  return data.paymentStatus; // "paid" | "pending" | "canceled"
}

```

## Key Repository Locations

The canonical documentation resides in [`docs/zh-cn/stage-2/backend/stripe-payment/index.md`](https://github.com/datawhalechina/easy-vibe/blob/main/docs/zh-cn/stage-2/backend/stripe-payment/index.md), which contains the complete integration guide, environment variable specifications, and flow diagrams.

Frontend redirect examples can be found in [`docs/.vitepress/theme/components/appendix/web-basics/UrlToBrowserDemo.vue`](https://github.com/datawhalechina/easy-vibe/blob/main/docs/.vitepress/theme/components/appendix/web-basics/UrlToBrowserDemo.vue), demonstrating how to handle browser navigation within the VitePress theme.

Environment variable handling patterns are illustrated in [`docs/.vitepress/theme/index.js`](https://github.com/datawhalechina/easy-vibe/blob/main/docs/.vitepress/theme/index.js) and `scripts/generate-sitemap.mjs`, showing how the project reads `process.env` values throughout the stack.

## Summary

- **Server-side price control** is mandatory; never accept price amounts from the client. Store `STRIPE_PRICE_PRO_MONTHLY` and `STRIPE_PRICE_PRO_YEARLY` in environment variables.
- **Webhook verification** provides the only reliable confirmation of payment success; the `success_url` callback alone is insufficient.
- **Supabase integration** requires storing `stripe_customer_id`, `stripe_subscription_id`, and subscription status in a dedicated table.
- **Local testing** requires the Stripe CLI (`stripe login` and `stripe listen`) to forward webhooks to your development environment.
- **Security** depends on verifying webhook signatures with `STRIPE_WEBHOOK_SECRET` before updating database records.

## Frequently Asked Questions

### How do I test Stripe webhooks locally?

Use the Stripe CLI to forward events to your local development server. Run `stripe login` to authenticate, then `stripe listen --forward-to localhost:3000/api/webhook` to route webhook events to your local endpoint. This allows you to test the `checkout.session.completed` events without deploying to production.

### Why should I not trust the Stripe success page?

The success page URL (`success_url`) is exposed to the client browser and can be accessed directly without completing payment. According to the repository documentation, you must rely on the `checkout.session.completed` webhook event to confirm payment before activating subscriptions or provisioning services.

### What database schema should I use for subscriptions?

Create a `subscriptions` table in Supabase containing `user_id` (foreign key), `stripe_customer_id`, `stripe_subscription_id`, `status` (enum: active, past_due, canceled), and `current_period_end` (timestamp). The webhook handler upserts records into this table when payment events occur.

### How do I handle different pricing tiers?

Define separate Price objects in the Stripe Dashboard for each tier (e.g., monthly vs. yearly), then store their respective `price_id` values in environment variables (`STRIPE_PRICE_PRO_MONTHLY`, `STRIPE_PRICE_PRO_YEARLY`). The backend selects the appropriate ID based on the plan parameter sent by the frontend, ensuring users cannot manipulate pricing.