How the Billing System Works in Awesome-GPT-Image-2: Stripe and Supabase Integration

The billing system in awesome-gpt-image-2 leverages Stripe for payment processing and Supabase for data persistence, implementing a dual-product architecture that handles both recurring membership subscriptions and one-time credit pack purchases through a secure checkout flow and idempotent webhook handlers.

The repository freestylefly/awesome-gpt-image-2 implements a complete billing subsystem designed for AI image generation services. It combines Stripe's payment infrastructure with Supabase's database to manage two distinct product types: recurring membership plans that grant monthly credits and one-off credit packs for immediate use. Understanding this architecture is essential for developers looking to integrate similar payment flows or customize the existing credit allocation logic.

Architecture Overview and Data Model

The system maintains all billing state within Supabase while delegating payment processing to Stripe. The core tables include membership_plans and credit_packs for product definitions, profiles for storing Stripe customer IDs, payment_orders for transaction tracking, user_memberships for subscription status, and credit_transactions for audit trails of credit balance changes.

This separation of concerns allows the application to handle complex subscription lifecycles while keeping financial data synchronized across services.

Fetching the Product Catalog

The billing flow begins at GET /api/billing/plans, implemented in api/billing/plans.js. This endpoint invokes getBillingCatalog from api/_lib/billing.js (lines 9-30), which queries the membership_plans and credit_packs tables to retrieve active products.

The helper functions formatPlan and formatPack normalize database rows into structured responses, including pricing, currency, and metadata. The endpoint also indicates which checkout providers—such as Stripe or Alipay—are available for each product.

// Example: retrieve billing catalog
fetch('/api/billing/plans')
  .then(r => r.json())
  .then(data => {
    console.log('Available plans:', data.plans);
    console.log('Available credit packs:', data.packs);
  });

Creating Checkout Sessions

When a user selects a product, the frontend POSTs to api/billing/checkout.js. The handler uses readJsonBody to parse and validate the request, then getBillingProduct retrieves the exact product definition from the catalog.

The system calls getOrCreateStripeCustomer to either reuse an existing Stripe customer ID stored in the profiles table or create a new one via Stripe's API. Simultaneously, createPaymentOrder inserts a pending record into payment_orders to track the transaction state.

The checkoutLineItem function maps the product's price, currency, and metadata to Stripe's line item format. The handler then creates a Stripe Checkout Session using stripe.checkout.sessions.create, updates the order via markOrderCheckoutCreated, and returns the session URL to the client.

async function startCheckout(productType, productId) {
  const body = { productType, productId };
  const resp = await fetch('/api/billing/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await resp.json();
  if (json.ok) {
    // Redirect user to Stripe Checkout
    window.location.href = json.url;
  } else {
    console.error('Checkout error:', json.error);
  }
}

Processing Stripe Webhooks

The api/billing/webhook.js handler receives Stripe events and verifies signatures using stripe.webhooks.constructEvent. For checkout.session.completed events, the system delegates to handleCheckoutCompleted, which branches based on product type:

  • Credit packs: completeCreditPackOrder grants the purchased credits via grantCredits and marks the order as completed.
  • Memberships: upsertMembershipFromSubscription creates or updates the user_memberships record, while grantMembershipCredits allocates the initial monthly credit allowance.

For recurring revenue, invoice.payment_succeeded events trigger credit grants for each billing cycle. Subscription lifecycle changes—customer.subscription.updated and customer.subscription.deleted—synchronize membership status and cancellation dates with the user_memberships table.

// Inside webhook.js – simplified flow
if (event.type === 'checkout.session.completed') {
  await handleCheckoutCompleted(client, stripe, event.data.object);
}

Credit Allocation and Idempotency

The core credit logic resides in api/_lib/billing.js. The grantCredits function invokes the Supabase RPC grant_user_credits to atomically update user balances. For membership benefits, grantMembershipCredits implements idempotency protection by calling hasTransactionWithMetadata to check credit_transactions for existing entries with identical Stripe event IDs.

If no duplicate exists, the system proceeds with the credit grant using the monthly amount defined in the membership_plans table. This pattern ensures that network retries or webhook redeliveries never result in duplicate credit allocations.

Subscription Management Portal

Authenticated users manage their subscriptions through api/billing/portal.js. This endpoint retrieves the user's stripeCustomerId from their profile and calls stripe.billingPortal.sessions.create to generate a secure session URL.

Users can upgrade or downgrade plans, update payment methods, or cancel subscriptions directly through Stripe's hosted interface, eliminating the need for custom subscription management UI components.

// Open subscription management UI
async function openPortal() {
  const resp = await fetch('/api/billing/portal', { method: 'POST' });
  const { url } = await resp.json();
  if (url) window.location.href = url;
}

Summary

  • The billing system in awesome-gpt-image-2 uses Stripe Checkout for payment collection and Supabase for persistent state management.
  • Two product types exist: recurring membership plans with monthly credits and one-time credit packs.
  • The checkout flow in api/billing/checkout.js creates payment_orders records and Stripe sessions before redirecting users.
  • Webhook handlers in api/billing/webhook.js finalize orders, grant credits through grantCredits, and synchronize subscription status.
  • Credit granting includes idempotency checks via hasTransactionWithMetadata to prevent duplicate allocations from webhook retries.
  • Users manage subscriptions through Stripe's Billing Portal generated by api/billing/portal.js.

Frequently Asked Questions

How does awesome-gpt-image-2 handle recurring subscription payments?

Recurring payments trigger Stripe invoice.payment_succeeded webhooks. The handler calls grantMembershipCredits to allocate monthly credits defined in the membership_plans table, first verifying against duplicates using hasTransactionWithMetadata. This keeps the user_memberships and credit_transactions tables synchronized with Stripe's billing cycle.

What happens when a user purchases a credit pack versus a membership?

For credit packs, handleCheckoutCompleted invokes completeCreditPackOrder, which immediately grants credits via grantCredits and marks the payment_orders record complete. For memberships, the system calls upsertMembershipFromSubscription to create or update the subscription record in user_memberships, then grants the initial monthly credits through grantMembershipCredits.

How does the system prevent duplicate credit grants from webhook retries?

The grantMembershipCredits function queries credit_transactions via hasTransactionWithMetadata to detect existing transactions with identical Stripe event metadata. If found, the operation aborts; otherwise, it proceeds with the grant_user_credits RPC call. This pattern ensures idempotent processing even during network failures or webhook redeliveries.

Which database tables store the billing state in awesome-gpt-image-2?

Supabase tables include membership_plans and credit_packs for product catalogs, profiles for Stripe customer IDs, payment_orders for transaction status, user_memberships for subscription details, and credit_transactions for credit balance changes. These tables maintain consistency with Stripe through the webhook handlers in api/billing/webhook.js.

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 →