How Stripe Is Integrated for Billing in awesome-gpt-image-2
awesome-gpt-image-2 uses Stripe as its primary payment provider for both subscription-based memberships and one-off credit-pack purchases, implementing a server-side architecture with checkout sessions, a billing portal, and signed webhook handlers.
The repository freestylefly/awesome-gpt-image-2 implements a robust Stripe billing integration that handles recurring subscriptions and one-time purchases through a layered, secure architecture. By combining Stripe Checkout, the Billing Portal, and webhook event processing, the application manages payment flows while keeping user entitlements and credit balances synchronized in Supabase. This server-side approach ensures PCI-compliant transaction handling and real-time state management across the entire billing lifecycle.
Configuration and Stripe Client Setup
The integration activates when the environment variable STRIPE_SECRET_KEY is present. In api/_lib/billing.js, the helper isStripeConfigured() checks for this variable (#L8‑L10), ensuring billing features only appear when properly configured.
A singleton Stripe client is lazily instantiated by getStripeClient() (#L12‑L19) using the secret key and a fixed API version (2026-02-25.clover). This client is reused by all billing endpoints to maintain connection efficiency and consistent configuration.
Checkout Flow for Subscriptions and Credit Packs
The checkout flow supports both membership subscriptions and one-time credit pack purchases through a unified endpoint.
Creating the Checkout Session
The checkout endpoint at api/billing/checkout.js validates the incoming request, loads the requested product via getBillingProduct(), and ensures a Stripe customer exists through getOrCreateStripeCustomer(). It then creates a payment order in Supabase using createPaymentOrder() before initiating the Stripe session.
The session is built with checkoutLineItem(product) (which structures the price data, currency, and metadata) and sent to Stripe via stripe.checkout.sessions.create() (#L60‑L104):
// /api/billing/checkout.js – simplified flow
import { getStripeClient, getBillingProduct, getOrCreateStripeCustomer,
createPaymentOrder, markOrderCheckoutCreated, checkoutLineItem } from '../../_lib/billing';
export default async function handler(req, res) {
const stripe = getStripeClient(); // ← Stripe client
const product = await getBillingProduct(client, type, id);
const customerId = await getOrCreateStripeCustomer({ client, stripe, user, profile });
const order = await createPaymentOrder(client, { userId, product, customerId });
const session = await stripe.checkout.sessions.create({
mode: product.type === 'membership' ? 'subscription' : 'payment',
customer: customerId,
line_items: [checkoutLineItem(product)],
metadata: { orderId: order.id, userId, productType: product.type, productId: product.id },
success_url: `${appUrl}/?billing=success&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${appUrl}/?billing=cancelled`
});
await markOrderCheckoutCreated(client, order.id, session);
res.json({ ok: true, url: session.url, orderId: order.id });
}
The resulting session URL is returned to the frontend, which redirects the user to Stripe’s hosted checkout page to complete payment securely.
Customer Management
During the checkout process, getOrCreateStripeCustomer() checks the profiles table for an existing stripeCustomerId. If none exists, it calls stripe.customers.create() and stores the new ID in Supabase, linking all future transactions to this customer record.
Self-Service Billing Portal
For subscription management, the portal endpoint at api/billing/portal.js generates a temporary session URL using Stripe’s Billing Portal. The handler calls stripe.billingPortal.sessions.create() (#L35‑L41) with the user’s stored stripeCustomerId and a return URL:
// /api/billing/portal.js
import { getStripeClient, getAppUrl } from '../../_lib/billing';
export default async function handler(req, res) {
const stripe = getStripeClient();
const session = await stripe.billingPortal.sessions.create({
customer: auth.profile.stripeCustomerId,
return_url: `${getAppUrl(req)}/?billing=portal_return`
});
res.json({ ok: true, url: session.url });
}
This allows users to manage subscriptions, view invoices, and update payment methods without custom UI development.
Webhook Processing and Event Handling
Stripe sends asynchronous events to api/billing/webhook.js to notify the system of payment outcomes. After validating the webhook signature using stripe.webhooks.constructEvent() (#L20‑L22), the handler dispatches to specific helpers based on the event type.
Handling Checkout Completion
The checkout.session.completed event triggers handleCheckoutCompleted() (#L21‑L34). This function distinguishes between credit-pack and membership purchases, updates the payment_orders table, and grants purchased credits by inserting records into credit_transactions. Metadata attached to the Stripe session—specifically orderId, userId, and productId—enables reliable mapping back to Supabase records.
Recurring Payments and Subscription Changes
For ongoing subscriptions, three additional events ensure data synchronization:
- invoice.payment_succeeded →
handleInvoicePaid()(#L76‑L82) updates membership status and refreshes credit balances when recurring payments process successfully. - customer.subscription.updated and customer.subscription.deleted →
handleSubscriptionChanged()(#L100‑L102) synchronizes the subscription state to theuser_membershipstable, handling cancellations, upgrades, or billing cycle changes.
// /api/billing/webhook.js – core logic
import { getStripeClient, readRawBody, isStripeConfigured } from '../../_lib/billing';
import { getSupabaseAdminClient } from '../../_lib/supabase';
export default async function handler(req, res) {
const stripe = getStripeClient();
const rawBody = await readRawBody(req);
const event = stripe.webhooks.constructEvent(
rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(client, stripe, event.data.object);
break;
case 'invoice.payment_succeeded':
await handleInvoicePaid(client, stripe, event.data.object);
break;
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
await handleSubscriptionChanged(client, event.data.object);
break;
}
res.json({ ok: true });
}
Data Persistence and Idempotency
All billing-related data lives in Supabase tables including payment_orders, profiles, user_memberships, and credit_transactions. Helper functions in api/_lib/billing.js—such as createPaymentOrder(), markOrderCheckoutCreated(), grantCredits(), and upsertMembershipFromSubscription()—encapsulate CRUD operations.
These helpers ensure idempotency by checking existing order statuses before processing, preventing duplicate credit grants or subscription activations if Stripe sends multiple webhook events for the same transaction. Metadata carriers on Stripe objects provide the internal IDs necessary to maintain consistency between Stripe’s state and the application’s database records.
Summary
- Configuration-driven activation: The integration enables only when
STRIPE_SECRET_KEYis present, with a singleton client pattern for efficient resource usage. - Dual purchase support: Handles both one-time credit packs and recurring membership subscriptions through a unified checkout endpoint.
- Self-service management: Leverages Stripe’s Billing Portal to provide customers with subscription and invoice management without custom UI.
- Reliable synchronization: Signed webhooks process checkout completions, invoice payments, and subscription changes to keep Supabase records and user entitlements in sync.
- Idempotent operations: Database helpers ensure safe, repeatable processing of payment events to prevent data inconsistencies.
Frequently Asked Questions
How does awesome-gpt-image-2 verify Stripe webhook authenticity?
The application verifies webhooks using stripe.webhooks.constructEvent() in api/billing/webhook.js‑L22), which validates the request signature against the STRIPE_WEBHOOK_SECRET environment variable. This cryptographic verification ensures the event originated from Stripe and was not tampered with in transit.
What database tables store billing information in this integration?
Billing data persists across four primary Supabase tables: payment_orders tracks transaction attempts, profiles stores stripeCustomerId links, user_memberships maintains active subscription states, and credit_transactions records granted or consumed credits. Helper functions in api/_lib/billing.js manage all interactions with these tables.
How does the system distinguish between credit packs and membership subscriptions?
The checkoutLineItem() helper and checkout session logic check product.type to set the Stripe session mode. For memberships, it uses mode: 'subscription', while credit packs use mode: 'payment'. Webhook handlers later reference this metadata in handleCheckoutCompleted() to apply the correct business logic—either inserting a subscription record or incrementing the credit balance.
Can users manage their subscriptions without contacting support?
Yes, the api/billing/portal.js endpoint generates a temporary URL to Stripe’s hosted Billing Portal using stripe.billingPortal.sessions.create(). Authenticated users can access this to update payment methods, view invoices, and cancel or upgrade subscriptions independently, with the return_url parameter ensuring they land back in the application afterward.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →