Integrating Stripe Payment Systems into a Vibe-Coded SaaS Application

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, 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):

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:

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:

<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:

// 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, 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, demonstrating how to handle browser navigation within the VitePress theme.

Environment variable handling patterns are illustrated in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →