How Stripe Webhooks Handle Subscription Events in Awesome-GPT-Image-2: Implementation and Architecture
The Awesome-GPT-Image-2 application processes Stripe subscription events through a secure webhook endpoint at api/billing/webhook.js that verifies signatures, routes events to specific handlers, and synchronizes subscription state with a Supabase database while managing user credit balances.
The Awesome-GPT-Image-2 project implements a robust billing system that leverages Stripe webhooks to maintain real-time synchronization between Stripe's subscription management and the application's internal credit-based membership system. Located in the api/billing/webhook.js endpoint, the implementation handles four critical event types to ensure users receive immediate access to purchased credits while maintaining data consistency across the SaaS platform.
Webhook Endpoint Security and Configuration
The webhook implementation follows a defensive programming pattern with three-layer configuration validation. Before processing any events, the system verifies that Supabase is configured, Stripe credentials are present via isStripeConfigured(), and the STRIPE_WEBHOOK_SECRET environment variable exists. Missing configuration triggers an immediate 500 response with the BILLING_NOT_CONFIGURED error code, preventing unauthorized or misconfigured endpoints from exposing sensitive payment data.
Signature Verification and Event Dispatch
Every incoming webhook request undergoes cryptographic signature verification to prevent tampering. The implementation reads the raw request body using readRawBody(req) and validates the payload against the stripe-signature header using stripe.webhooks.constructEvent(). Invalid signatures return a 400 status, while valid events proceed through a switch statement that routes to specialized handlers based on the event type.
const rawBody = await readRawBody(req);
event = stripe.webhooks.constructEvent(
rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
Source: api/billing/webhook.js:20-22
Subscription Lifecycle Event Handlers
The webhook endpoint processes four distinct Stripe event types through dedicated handler functions that ensure the Supabase database remains synchronized with Stripe's subscription state.
Checkout Session Completion
The checkout.session.completed event, processed by handleCheckoutCompleted (lines 21-73), represents the primary entry point for new subscriptions. This handler distinguishes between one-time credit pack purchases and recurring membership subscriptions by examining the product type metadata.
For membership purchases, the handler retrieves the subscription object via stripe.subscriptions.retrieve(), then calls upsertMembershipFromSubscription() to create or update the membership record in Supabase. The system simultaneously marks the associated payment_orders row as completed and invokes grantMembershipCredits() to immediately provision the user's credit allowance.
if (productType === 'membership') {
const subscriptionId = typeof session.subscription === 'string'
? session.subscription
: session.subscription?.id;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await upsertMembershipFromSubscription(client, subscription, {
userId: session.metadata?.userId,
planId: session.metadata?.productId,
customerId: typeof session.customer === 'string' ? session.customer : ''
});
// ...grant credits and update payment_orders...
}
Source: handleCheckoutCompleted
Recurring Invoice Payments
Recurring billing cycles trigger invoice.payment_succeeded events handled by handleInvoicePaid (lines 76-98). This handler intentionally skips events where billing_reason equals subscription_create to avoid double-processing initial payments already handled by checkout completion.
For renewal payments, the handler fetches the associated subscription, updates the membership record via upsertMembershipFromSubscription(), and grants credits through grantMembershipCredits() with metadata tracking the Stripe invoice ID and subscription reference.
await grantMembershipCredits(client, {
userId,
planId,
source: 'stripe_membership_invoice',
metadata: {
stripeInvoiceId: invoice.id,
stripeSubscriptionId: subscriptionId,
billingReason: invoice.billing_reason || ''
}
});
Source: handleInvoicePaid
Subscription Updates and Deletions
Both customer.subscription.updated and customer.subscription.deleted events route to handleSubscriptionChanged (lines 100-102), which mirrors the latest subscription state into the Supabase memberships table. Unlike payment handlers, this function does not grant additional credits, as billing cycle changes generate separate invoice events. When subscriptions are deleted, the helper function marks memberships as inactive, ensuring cancelled subscriptions immediately cease credit grants without requiring manual intervention.
Database Synchronization and Credit Management
The webhook system maintains strict data consistency through utility functions defined in api/_lib/billing.js. The upsertMembershipFromSubscription() function implements idempotent updates to the memberships table, ensuring repeated webhook deliveries do not create duplicate records.
Credit allocation occurs through grantMembershipCredits(), which writes to the user's balance with detailed metadata including the Stripe invoice ID, subscription ID, and billing reason for complete audit trails. All database operations use the admin Supabase client from api/_lib/supabase.js to ensure sufficient privileges for updating restricted user records.
Error Handling and Retry Behavior
All webhook handlers wrap business logic in try-catch blocks that log detailed error summaries and return 500 status codes with the WEBHOOK_PROCESSING_FAILED error identifier. This approach ensures Stripe's retry mechanism receives proper failure signals while preventing partial database updates that could corrupt subscription state. Any exception inside the handlers results in immediate termination with a 500 response, signaling Stripe to retry the webhook delivery.
Summary
- The
api/billing/webhook.jsendpoint processes four Stripe event types through specialized handlers that mirror subscription state to Supabase - Cryptographic signature verification using
stripe.webhooks.constructEvent()withprocess.env.STRIPE_WEBHOOK_SECRETprevents unauthorized webhook calls - The
handleCheckoutCompletedfunction distinguishes between credit packs and memberships while initializing subscription records viaupsertMembershipFromSubscription() - Recurring payments trigger
handleInvoicePaid, which provisions credits while skipping initial subscription creation events to prevent double-crediting handleSubscriptionChangedmanages lifecycle updates including cancellations, ensuring immediate access revocation for expired subscriptions- Helper functions in
api/_lib/billing.jsprovide idempotent database operations and comprehensive audit logging through metadata tracking
Frequently Asked Questions
How does the webhook verify that requests actually come from Stripe?
The implementation verifies webhook authenticity by reading the raw request body with readRawBody(req) and passing it to stripe.webhooks.constructEvent() alongside the stripe-signature header and the STRIPE_WEBHOOK_SECRET environment variable. This cryptographic verification ensures the payload originated from Stripe and has not been tampered with during transmission. Invalid signatures immediately return a 400 response before any business logic executes.
What happens when a user upgrades or cancels their subscription?
The system processes customer.subscription.updated and customer.subscription.deleted events through the handleSubscriptionChanged function (lines 100-102), which calls upsertMembershipFromSubscription() to reflect the new status in the Supabase memberships table. Cancellations immediately mark memberships as inactive, stopping credit grants without waiting for the billing period to end, while upgrades modify the plan ID and subscription metadata for the next billing cycle.
How does the implementation prevent double-crediting users for the same subscription period?
The handleInvoicePaid function specifically checks the billing_reason field and skips processing when it equals subscription_create, since initial credits are granted during checkout.session.completed handling. This separation of concerns ensures users receive credits exactly once per payment, regardless of Stripe's retry behavior or event ordering. The idempotent design of grantMembershipCredits() provides additional protection against duplicate credit grants.
Which database tables does the webhook modify during subscription processing?
The webhook interacts with multiple Supabase tables including payment_orders (marking purchases as completed), memberships (storing active subscription state via upsertMembershipFromSubscription()), and user credit balance tables (via grantMembershipCredits()). All operations use the admin Supabase client to ensure atomic updates across these tables, maintaining referential integrity between Stripe's subscription state and the application's internal billing records.
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 →