The boilerplate provides comprehensive webhook event processing that works with both payment systems: Better Auth handles standard events automatically, while custom processing handles advanced features like guest checkout.
Hybrid System: Better Auth processes standard subscription events automatically, while custom event handlers manage advanced features like guest checkout and installments from the Custom Boilerplate System.
Perfect for: Real-time payment processing, automatic subscription updates, and reliable event-driven business logic with robust error handling.
The webhook system uses a unified approach that serves both payment systems:
Important: Both Better Auth and Custom Boilerplate systems use the same webhook endpoint (/api/auth/stripe/webhook) but events are processed differently based on their source and metadata.
Automatic Handling:
Endpoint: /api/auth/stripe/webhook
The webhook endpoint is automatically configured through the Better Auth Stripe plugin:
// From src/lib/better-auth/auth.ts
stripe({
stripeClient,
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,
createCustomerOnSignUp: true,
subscription: {
enabled: true,
// Automatic event handlers...
onSubscriptionComplete: async ({ subscription }) => { /* ... */ },
onSubscriptionUpdate: async ({ subscription }) => { /* ... */ },
onSubscriptionCancel: async ({ subscription }) => { /* ... */ },
onSubscriptionDeleted: async ({ subscription }) => { /* ... */ },
},
onEvent: onStripeEvent, // Custom event handler
})Additional business logic is processed in the custom event handler:
// From src/lib/stripe/stripe-events.ts
export const onStripeEvent = async (event: Stripe.Event) => {
console.log(`Processing Stripe event: ${event.type}`)
// Custom business logic for events not handled by Better Auth
switch (event.type) {
case 'invoice.payment_failed':
// Handle failed payments
break
case 'customer.subscription.trial_will_end':
// Send trial ending notifications
break
default:
// Log unhandled events
console.log(`Unhandled event type: ${event.type}`)
}
}Events Processed:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.pausedcustomer.subscription.resumedAutomatic Actions:
Custom Boilerplate System Only: The following advanced event processing is specific to the Custom Boilerplate System's features like guest checkout and installments. Better Auth handles its events automatically.
The boilerplate includes sophisticated custom event processing for advanced checkout scenarios that Better Auth doesn't handle automatically.
Advanced Guest Checkout with Account Creation:
// From src/lib/stripe/stripe-events.ts
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const isCustomCheckout = session.metadata?.source === 'custom_checkout'
// Handle guest checkout with automatic user creation
if (isCustomCheckout) {
await handleGuestCheckoutSessionCompleted(session)
}
// Handle installment checkout
const isInstallmentCheckout = session.metadata?.source === 'installment_checkout'
if (isInstallmentCheckout) {
await handleInstallmentCheckoutSessionCompleted(session)
}
break
}Guest Checkout Workflow:
referenceId: 'guest' or guest_checkout: 'true')Payment Installments Support:
// From src/lib/stripe/stripe-events.ts
async function handleInstallmentCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
// 1. Validate installment data (schedule_id, number_of_payments)
const installmentData = validateInstallmentCheckoutData(session, metadata)
// 2. Handle user creation (guest or authenticated)
const finalUser = isGuestCheckout
? await createGuestUser(customerData)
: await getAuthenticatedUser(customerEmail)
// 3. Create subscription with installment periods
await createInstallmentSubscription(installmentData, finalUser)
}Additional events can also be handled in your custom event handler:
// Custom handling for payment failures
case 'invoice.payment_failed':
const invoice = event.data.object as Stripe.Invoice
// Custom business logic
await handlePaymentFailure({
customerId: invoice.customer as string,
invoiceId: invoice.id,
amountDue: invoice.amount_due,
attemptCount: invoice.attempt_count,
})
break// Custom trial ending notifications
case 'customer.subscription.trial_will_end':
const subscription = event.data.object as Stripe.Subscription
await sendTrialExpirationNotification({
customerId: subscription.customer as string,
subscriptionId: subscription.id,
trialEnd: new Date(subscription.trial_end! * 1000),
})
break// Handle metered billing events
case 'invoice.upcoming':
const upcomingInvoice = event.data.object as Stripe.Invoice
// Calculate usage charges before invoice finalization
await calculateUsageCharges({
customerId: upcomingInvoice.customer as string,
periodStart: new Date(upcomingInvoice.period_start * 1000),
periodEnd: new Date(upcomingInvoice.period_end * 1000),
})
breakSubscription Created:
onSubscriptionComplete: async ({ subscription }) => {
// Automatic Better Auth processing:
// 1. Update user's subscription status in database
// 2. Send welcome email notification
// 3. Grant access to premium features
// 4. Update usage limits
// 5. Log subscription event for analytics
}Subscription Updated:
onSubscriptionUpdate: async ({ subscription }) => {
// Automatic Better Auth processing:
// 1. Update subscription details in database
// 2. Adjust user access levels
// 3. Send plan change confirmation email
// 4. Update feature limitations
// 5. Prorate billing if necessary
}Subscription Canceled:
onSubscriptionCancel: async ({ subscription }) => {
// Automatic Better Auth processing:
// 1. Mark subscription as canceled
// 2. Send cancellation confirmation
// 3. Schedule access removal (if immediate)
// 4. Trigger win-back campaign
// 5. Update customer success metrics
}Successful Payments:
Failed Payments:
The Better Auth plugin automatically verifies webhook signatures:
// Automatic signature verification by Better Auth
stripe({
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET, // Used for verification
})Security Features:
Webhook Reliability:
Log Information:
Log Example:
export const onStripeEvent = async (event: Stripe.Event) => {
console.log(`Processing Stripe event: ${event.type}`)
console.log(`Event ID: ${event.id}`)
console.log(`Created: ${new Date(event.created * 1000).toISOString()}`)
try {
// Process event
await processCustomEvent(event)
console.log(`✅ Successfully processed ${event.type}`)
} catch (error) {
console.error(`❌ Error processing ${event.type}:`, error)
throw error // Let Better Auth handle retry logic
}
}Metrics Tracking:
Install and configure Stripe CLI for local webhook forwarding:
# Install Stripe CLI
# Forward webhooks to local development server
stripe listen --forward-to localhost:3000/api/auth/stripe/webhookUse Stripe CLI to trigger test events:
# Test subscription creation
stripe trigger customer.subscription.created
# Test payment success
stripe trigger invoice.payment_succeeded
# Test payment failure
stripe trigger invoice.payment_failedEnsure webhook reliability:
Maintain webhook security:
Optimize webhook performance:
Local Development: The boilerplate includes comprehensive webhook testing tools for local development, allowing you to test subscription flows without real payments.
The boilerplate uses Stripe CLI for local webhook development:
Install the Stripe CLI for your platform:
# macOS
brew install stripe/stripe-cli/stripe
# Windows
scoop install stripe
# Linux - download from GitHub releases
# https://github.com/stripe/stripe-cli/releasesLogin to your Stripe account:
stripe loginThe boilerplate includes a pre-configured command to forward webhooks to your local development server:
pnpm stripe:listenThis runs: stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
What it does:
When you start the listener, Stripe CLI provides a webhook signing secret:
# Output example:
> Ready! Your webhook signing secret is whsec_1a2b3c...Add this to your .env.local:
STRIPE_WEBHOOK_SECRET=whsec_1a2b3c...The boilerplate includes a comprehensive webhook testing script at /test-stripe-webhooks.sh:
Important: Start the webhook listener (pnpm stripe:listen) before running tests to receive the events.
Script Features:
# Make the script executable
chmod +x test-stripe-webhooks.sh
# Run the interactive testing script
./test-stripe-webhooks.shMenu Options:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deletedEfficient webhook testing workflow:
pnpm devpnpm stripe:listen.env.local# Run the interactive testing script
./test-stripe-webhooks.sh
# Or trigger individual events:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deletedMonitor the results:
Use the script's debug features:
For quick manual testing without the script:
# Test subscription creation
stripe trigger checkout.session.completed
# Test subscription update (renewal)
stripe trigger customer.subscription.updated
# Test subscription cancellation
stripe trigger customer.subscription.deletedVerify webhook functionality:
pnpm stripe:listen)Local Testing Ready! You can now test complete subscription workflows locally without processing real payments, ensuring your webhook handling works perfectly before going live.
Webhook system ready! Your application now processes Stripe events in real-time with reliable business logic execution and comprehensive error handling.