How Stripe Is Integrated for Payments in awesome-gpt-image-2: Complete Implementation Guide

The awesome-gpt-image-2 repository implements Stripe payments through a three-layer architecture using environment-based configuration, a server-side billing library in api/_lib/billing.js, and dedicated checkout/webhook endpoints that handle both subscription memberships and one-off credit packs.

This open-source image generation platform uses Stripe as its primary payment provider, backed by Supabase for order persistence and user management. The integration supports recurring billing for membership plans and immediate payments for credit packs, all synchronized through Stripe Checkout Sessions and webhook events. Understanding this Stripe integration for payments reveals a production-ready pattern for handling dual-mode billing in modern web applications.

Architecture Overview

The payment system splits responsibilities across three logical layers:

  1. Configuration Layer – Environment variables store API keys and webhook secrets
  2. Billing Library – api/_lib/billing.js encapsulates Stripe SDK initialization, customer management, and Supabase order operations
  3. HTTP Endpoints – api/billing/checkout.js creates sessions while api/billing/webhook.js processes asynchronous events

This separation ensures that payment logic remains testable and that raw webhook payloads can be verified before touching database records.

Configuration and Environment Setup

Stripe credentials are never hardcoded. The repository expects two critical environment variables defined in .env.example:

  • STRIPE_SECRET_KEY – Server-side API key for creating checkout sessions
  • STRIPE_WEBHOOK_SECRET – Endpoint secret for verifying webhook signatures

The isStripeConfigured() function in api/_lib/billing.js validates that STRIPE_SECRET_KEY exists before any payment operations proceed, preventing runtime errors in misconfigured deployments.

Server-Side Billing Library

The core abstraction lives in api/_lib/billing.js, which wraps the Stripe SDK and Supabase client.

Stripe Client Initialization

The getStripeClient() function lazily instantiates the Stripe SDK with a pinned API version:

// api/_lib/billing.js
import Stripe from 'stripe';

export function getStripeClient() {
  if (!stripeClient) {
    stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY, {
      apiVersion: '2026-02-25.clover', // Pinned version for stability
    });
  }
  return stripeClient;
}

Pinning the API version prevents unexpected breaking changes when Stripe updates its SDK.

Customer Management

The getOrCreateStripeCustomer() helper ensures every user has a corresponding Stripe customer object. It checks the profiles table for an existing stripe_customer_id; if absent, it creates a new customer via stripe.customers.create() and persists the ID back to Supabase. This links all future transactions to a consistent customer record.

Order Persistence

Before generating a checkout URL, the system creates a record in the payment_orders table using createPaymentOrder(). This record tracks the user_id, product details, amount, currency, and initial status. After Stripe confirms the session creation, markOrderCheckoutCreated() updates the order with the Stripe session ID, creating an audit trail that withstands webhook delays or failures.

Checkout Flow Implementation

The api/billing/checkout.js endpoint orchestrates the purchase initiation.

Session Creation Process

When a POST request arrives with { productType, productId }, the handler:

  1. Validates Stripe configuration via isStripeConfigured()
  2. Retrieves product details (membership plan or credit pack) from getBillingProduct()
  3. Ensures a Stripe customer exists via getOrCreateStripeCustomer()
  4. Creates a pending order in Supabase
  5. Builds a dynamic checkout payload using checkoutLineItem() for price formatting

The session mode switches based on product type: subscription for memberships or payment for one-off credit packs.

Metadata Linking Strategy

Critical metadata attaches to every Stripe object:

// api/billing/checkout.js
const metadata = {
  orderId: order.id,
  userId: auth.user.id,
  productType,
  productId
};

if (productType === 'membership') {
  sessionPayload.subscription_data = { metadata };
} else {
  sessionPayload.payment_intent_data = { metadata };
}

This metadata enables the webhook handler to reconcile Stripe events with Supabase records without relying on fragile string parsing.

Webhook Handling for Payment Confirmation

The api/billing/webhook.js endpoint processes Stripe’s asynchronous events. It configures bodyParser: false to preserve the raw request body required for signature verification.

Signature Verification

// api/billing/webhook.js
const rawBody = await readRawBody(req);
const signature = req.headers['stripe-signature'];

const event = stripe.webhooks.constructEvent(
  rawBody,
  signature,
  process.env.STRIPE_WEBHOOK_SECRET
);

Without this verification, attackers could forge payment success notifications.

Event Processing Logic

The handler dispatches three event types:

  • checkout.session.completed – Finalizes credit-pack orders via completeCreditPackOrder() or activates memberships through upsertMembershipFromSubscription() and grantMembershipCredits()
  • invoice.payment_succeeded – Grants recurring monthly credits when subscription invoices pay
  • customer.subscription.updated / deleted – Syncs membership status changes back to Supabase

Each handler uses the metadata fields embedded during checkout to locate the original payment_orders record and update the user's credit balance or membership tier accordingly.

Complete Payment Flow Example

A typical purchase follows this sequence:

// Front-end initiates purchase
const { data } = await axios.post('/api/billing/checkout', {
  productType: 'credit_pack',
  productId: 'pack_100_credits'
});

// Redirect to Stripe hosted checkout
window.location.href = data.url;

Once the user completes payment, Stripe POSTs to /api/billing/webhook:

// Server processes webhook
case 'checkout.session.completed': {
  const session = event.data.object;
  // Retrieve metadata to find order
  const { orderId, userId, productType } = session.metadata;
  
  if (productType === 'credit_pack') {
    await completeCreditPackOrder(supabase, orderId);
  } else {
    const subscription = await stripe.subscriptions.retrieve(
      session.subscription
    );
    await upsertMembershipFromSubscription(supabase, userId, subscription);
    await grantMembershipCredits(supabase, userId);
  }
  break;
}

The user's account reflects the purchase immediately after webhook processing, with the payment_orders table marking the transaction as completed.

Summary

  • Stripe integration for payments in awesome-gpt-image-2 relies on environment variables (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET) configured in .env.example
  • The api/_lib/billing.js library centralizes SDK initialization, customer management, and Supabase order operations
  • Checkout sessions are created in api/billing/checkout.js with embedded metadata linking Stripe objects to Supabase records
  • Webhook verification in api/billing/webhook.js uses raw body parsing and signature validation to securely process checkout.session.completed and subscription events
  • The architecture distinguishes between subscription mode for memberships and payment mode for credit packs, handling both recurring and one-time billing patterns

Frequently Asked Questions

How does awesome-gpt-image-2 handle Stripe customer creation?

The system uses getOrCreateStripeCustomer() in api/_lib/billing.js to check if the user's profile record contains a stripe_customer_id. If missing, it calls stripe.customers.create() with the user's email and UUID, then stores the returned customer ID in the Supabase profiles table, ensuring all subsequent charges associate with the correct Stripe entity.

What webhook events does the integration process?

The endpoint in api/billing/webhook.js explicitly handles three event types: checkout.session.completed for initial purchase confirmation, invoice.payment_succeeded for recurring subscription payments, and customer.subscription.updated or deleted for membership status changes. Each event updates the Supabase database to reflect the current billing state.

How are one-off credit packs differentiated from subscriptions in the code?

The differentiation occurs in api/billing/checkout.js where the mode parameter is set dynamically: mode: productType === 'membership' ? 'subscription' : 'payment'. Memberships use Stripe's subscription engine with recurring billing, while credit packs use one-time payment intents. The webhook handlers then branch based on this productType metadata to either grant one-time credits or set up recurring membership benefits.

Where are Stripe API credentials configured?

Credentials are configured via environment variables listed in .env.example: STRIPE_SECRET_KEY for server-side API calls and STRIPE_WEBHOOK_SECRET for verifying incoming webhooks. The isStripeConfigured() function validates their presence at runtime, and getStripeClient() initializes the SDK with a pinned API version (2026-02-25.clover) to ensure consistent behavior across deployments.

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 →