Stripe Implementation
Overview
Section titled “Overview”FloTorch Console integrates with Stripe to provide comprehensive payment processing, subscription management, and usage-based billing. The implementation supports customer management, subscription lifecycle handling, webhook processing, and usage metering for accurate billing.
Architecture
Section titled “Architecture”The Stripe implementation consists of several key components:
- Stripe Utility Functions (
server/utils/stripe.ts
) - Core Stripe API interactions - Webhook Processing (
server/api/webhooks/stripe/
) - Real-time event handling - Billing Portal (
server/api/payments/stripe/
) - Customer self-service portal - Usage Metering (
server/utils/metering.ts
) - Usage tracking and reporting - Subscription Management (
server/plugins/03.subscriptions.ts
) - Automatic subscription creation
Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”Configure Stripe in your nuxt.config.ts
:
export default defineNuxtConfig({ runtimeConfig: { stripe: { trialDays: 30, // Trial period in days priceId: "price_xxxxx", // Base subscription price ID requestDimension: "additional_requests", // Usage dimension for requests usersDimension: "additional_users", // Usage dimension for users requestsPriceId: "price_xxxxx", // Usage-based pricing for requests usersPriceId: "price_xxxxx", // Usage-based pricing for users secretKey: "sk_xxxxx", // Stripe secret key webhookSecret: "whsec_xxxxx", // Webhook endpoint secret allowPromotionCodes: false, // Enable/disable promotion codes automaticTax: false, // Enable/disable automatic tax }, },});
Required Stripe Setup
Section titled “Required Stripe Setup”-
Create Products and Prices in Stripe Dashboard:
- Base subscription product with recurring pricing
- Usage-based products for requests and users
- Configure billing dimensions for metered usage
-
Configure Webhook Endpoint:
- URL:
https://yourdomain.com/api/webhooks/stripe
- Events to listen for:
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.subscription.paused
customer.subscription.resumed
invoice.payment_succeeded
invoice.payment_failed
customer.subscription.trial_will_end
- URL:
-
Set up Billing Portal in Stripe Dashboard:
- Configure allowed features (payment methods, invoices, etc.)
- Set return URL to your application
Core Components
Section titled “Core Components”Stripe Utility Functions
Section titled “Stripe Utility Functions”The useStripe()
composable provides all Stripe API interactions:
export const useStripe = () => { const stripeInstance = getStripeInstance();
return { stripe: stripeInstance, createCustomer, // Create new Stripe customer createSubscription, // Create subscription for customer createCustomerAndSubscription, // Combined customer + subscription creation createOrGetCustomer, // Find existing or create new customer reportUsage, // Report usage for metered billing createPortalLink, // Generate billing portal session verifyWebhookSignature, // Verify webhook authenticity getSubscription, // Retrieve subscription details };};
Customer Management
Section titled “Customer Management”// Create a new customerconst customer = await createCustomer({ orgId: "123", email: "user@example.com", name: "John Doe", metadata: { orgId: "123", orgUid: "uuid-here", source: "flotorch_console", },});
// Find existing customer or create new oneconst customer = await createOrGetCustomer( "123", // orgId "user@example.com", // email "John Doe", // name { orgId: "123", orgUid: "uuid", source: "flotorch_console" });
Subscription Management
Section titled “Subscription Management”// Create subscription with multiple price itemsconst subscription = await createSubscription({ orgId: "123", customerId: "cus_xxxxx", trialDays: 30, prices: [ { priceId: "price_base", quantity: 1 }, { priceId: "price_users" }, // Usage-based { priceId: "price_requests" }, // Usage-based ], metadata: { orgId: "123", orgUid: "uuid", source: "flotorch_console", },});
// Combined customer and subscription creationconst { customer, subscription } = await createCustomerAndSubscription({ orgId: "123", email: "user@example.com", name: "John Doe", trialDays: 30, prices: [ { priceId: "price_base", quantity: 1 }, { priceId: "price_users" }, { priceId: "price_requests" }, ],});
Usage Reporting
Section titled “Usage Reporting”// Report usage for metered billingconst response = await reportUsage( "cus_xxxxx", // customerId "additional_requests", // eventName (dimension) Date.now(), // timestamp 100, // value "usage-uuid" // unique identifier);
Billing Portal
Section titled “Billing Portal”// Create billing portal sessionconst portalUrl = await createPortalLink("cus_xxxxx");// Returns: https://billing.stripe.com/session/xxxxx
Webhook Processing
Section titled “Webhook Processing”The webhook handler (server/api/webhooks/stripe/index.post.ts
) processes real-time events from Stripe:
Supported Events
Section titled “Supported Events”Event Type | Description | Action |
---|---|---|
customer.subscription.created | New subscription created | Link subscription to organization |
customer.subscription.updated | Subscription modified | Update subscription status and end date |
customer.subscription.deleted | Subscription canceled | Mark as canceled, set end date |
customer.subscription.paused | Subscription paused | Update status to paused |
customer.subscription.resumed | Subscription resumed | Reactivate subscription |
invoice.payment_succeeded | Payment successful | Update subscription status to active |
invoice.payment_failed | Payment failed | Update status to past_due |
customer.subscription.trial_will_end | Trial ending soon | Update trial status |
Webhook Security
Section titled “Webhook Security”// Verify webhook signatureconst stripeEvent = await verifyWebhookSignature(rawBody, signature);
// Process event based on typeswitch (stripeEvent.type) { case "customer.subscription.created": await handleSubscriptionCreated(stripeEvent.data.object); break; // ... other event handlers}
Race Condition Handling
Section titled “Race Condition Handling”The webhook handler includes retry logic to handle race conditions:
async function findOrgWithRetry(customerId: string, maxRetries: number = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { const org = await db.query.orgs.findFirst({ where: eq(tables.orgs.stripeCustomerId, customerId), });
if (org) return org;
if (attempt < maxRetries) { const delay = 500 * Math.pow(2, attempt - 1); // Exponential backoff await new Promise(resolve => setTimeout(resolve, delay)); } } return null;}
Database Schema
Section titled “Database Schema”The Stripe integration uses the following database fields in the orgs
table:
export const orgs = pgTable("orgs", { // ... other fields stripeCustomerId: text().unique(), // Stripe customer ID stripeSubscriptionId: text().unique(), // Stripe subscription ID subscriptionStatus: text().notNull().default("trialing").$type<StripeSubscriptionStatus>(), subscriptionEnd: timestamp(), // Subscription end date});
Subscription Status Types
Section titled “Subscription Status Types”export const StripeSubscriptionStatusSchema = z.enum([ "trialing", // Trial period active "active", // Subscription active and paid "past_due", // Payment failed, retrying "canceled", // Subscription canceled "incomplete", // Initial payment failed "incomplete_expired", // Initial payment expired "paused", // Subscription paused "unpaid", // Payment failed, no retries]);
Usage Metering
Section titled “Usage Metering”The system tracks and reports usage for metered billing:
Usage Calculation
Section titled “Usage Calculation”export async function calculateReportableUsage(orgId: number) { const startOfMonth = new Date(); startOfMonth.setDate(1); startOfMonth.setHours(0, 0, 0, 0);
// Calculate users and requests for the current month const users = await calculateUserUsage(orgId, startOfMonth); const requests = await calculateRequestUsage(orgId, startOfMonth);
return { toReport: { users, requests }, alreadyReported: { /* previously reported usage */ }, };}
Usage Reporting
Section titled “Usage Reporting”// Report usage to Stripeexport async function reportToThirdParty(payload: { orgId: number; users: number; requests: number;}) { const org = await getOrgBillingInfo(payload.orgId);
if (org.stripeCustomerId) { const { reportUsage } = useStripe();
// Report users usage await reportUsage( org.stripeCustomerId, "additional_users", Date.now(), payload.users, `users-${payload.orgId}-${Date.now()}` );
// Report requests usage await reportUsage( org.stripeCustomerId, "additional_requests", Date.now(), payload.requests, `requests-${payload.orgId}-${Date.now()}` ); }}
Subscription Lifecycle
Section titled “Subscription Lifecycle”Automatic Subscription Creation
Section titled “Automatic Subscription Creation”The 03.subscriptions.ts
plugin automatically creates Stripe subscriptions for organizations without billing:
// Find orgs without any billing setupconst orgsWithoutSubscription = await db.query.orgs.findMany({ where: and( isNull(tables.orgs.stripeSubscriptionId), isNull(tables.orgs.awsAccountId), isNull(tables.orgs.azureSubscriptionId), ),});
// Create Stripe subscription for each orgfor (const org of orgsWithoutSubscription) { const { subscription, customer } = await createCustomerAndSubscription({ email: owner.user.email, name: `${owner.user.firstName} ${owner.user.lastName} (${org.name})`, orgId: org.id.toString(), prices: [ { priceId: stripeConfig.priceId, quantity: 1 }, { priceId: stripeConfig.usersPriceId }, { priceId: stripeConfig.requestsPriceId }, ], trialDays: stripeConfig.trialDays, });
// Update org with Stripe details await db.update(tables.orgs).set({ stripeCustomerId: customer.id, stripeSubscriptionId: subscription.id, subscriptionStatus: subscription.status, subscriptionEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : null, });}
Subscription Status Management
Section titled “Subscription Status Management”The system handles various subscription states:
// Handle subscription updatesasync function handleSubscriptionUpdated(subscription: Stripe.Subscription) { let subscriptionEnd: Date | null = null;
if (subscription.cancel_at) { // Scheduled cancellation subscriptionEnd = new Date(subscription.cancel_at * 1000); } else if (subscription.canceled_at) { // Immediate cancellation subscriptionEnd = new Date(subscription.canceled_at * 1000); } else { // Normal subscription period subscriptionEnd = getNextPeriodEnd(subscription); }
await db.update(tables.orgs).set({ subscriptionStatus: subscription.status, subscriptionEnd, });}
Billing Portal Integration
Section titled “Billing Portal Integration”Customers can manage their subscriptions through Stripe’s billing portal:
export default eventHandler({ onRequest: (event) => useAuthOrgValidation(event), handler: async (event) => { const org = await getOrgFromContext(event);
if (!org.stripeCustomerId) { throw createError({ statusCode: 404, statusMessage: "Stripe customer not found", }); }
const portalUrl = await createPortalLink(org.stripeCustomerId); return { url: portalUrl }; },});
Error Handling
Section titled “Error Handling”The implementation includes comprehensive error handling:
// Stripe configuration validationconst getStripeInstance = () => { const stripeSecretKey = runtimeConfig.stripe?.secretKey; if (!stripeSecretKey || !runtimeConfig.stripe.webhookSecret) { throw createError({ statusCode: 500, data: { detail: { title: "Stripe not configured", description: "Stripe secret key or webhook secret is missing.", }, }, }); } return new Stripe(stripeSecretKey);};
// Webhook signature verificationtry { stripeEvent = await verifyWebhookSignature(rawBody, signature);} catch { throw createError({ statusCode: 400, statusMessage: "Invalid webhook signature", });}
Security Considerations
Section titled “Security Considerations”- Webhook Signature Verification: All webhooks are verified using Stripe’s signature validation
- Environment Variables: Sensitive keys are stored in runtime configuration
- Database Encryption: Customer IDs and subscription IDs are stored securely
- Access Control: Billing portal access requires organization authentication
- Idempotency: Usage reporting uses idempotency keys to prevent duplicate charges
Monitoring and Logging
Section titled “Monitoring and Logging”The system includes comprehensive logging for monitoring:
// Log subscription eventsconsole.log(`Updated org ${org.id} with subscription ${subscription.id}, status: ${subscription.status}`);
// Log webhook processingconsole.log(`Handling subscription created event for subscription ${stripeEvent.data.object.id}`);
// Log errors with contextconsole.error("error", "Subscriptions plugin failed:", error);
Testing
Section titled “Testing”Test Mode Configuration
Section titled “Test Mode Configuration”For testing, use Stripe’s test mode:
// Use test keysstripe: { secretKey: "sk_test_xxxxx", // Test secret key webhookSecret: "whsec_test_xxxxx", // Test webhook secret // ... other test configuration}
Webhook Testing
Section titled “Webhook Testing”Use Stripe CLI for local webhook testing:
# Install Stripe CLIstripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test eventsstripe trigger customer.subscription.createdstripe trigger invoice.payment_succeeded
Best Practices
Section titled “Best Practices”- Idempotency: Always use idempotency keys for usage reporting
- Error Handling: Implement comprehensive error handling for all Stripe operations
- Webhook Security: Always verify webhook signatures
- Race Conditions: Use retry logic for webhook processing
- Monitoring: Log all important events for debugging and monitoring
- Testing: Use Stripe’s test mode for development and testing
- Data Consistency: Keep local database in sync with Stripe data
- Customer Experience: Provide clear billing portal access and error messages
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
Webhook Signature Verification Failed
- Check webhook secret configuration
- Ensure raw body is used for signature verification
- Verify webhook endpoint URL
-
Subscription Not Created
- Check Stripe API keys and permissions
- Verify price IDs exist in Stripe
- Check trial days configuration
-
Usage Not Reported
- Verify customer has active subscription
- Check usage dimension names match Stripe configuration
- Ensure idempotency keys are unique
-
Billing Portal Access Denied
- Verify customer ID exists in database
- Check organization authentication
- Ensure billing portal is configured in Stripe
Debugging Tools
Section titled “Debugging Tools”- Stripe Dashboard: Monitor events, customers, and subscriptions
- Stripe CLI: Test webhooks and API calls locally
- Application Logs: Check server logs for error messages
- Database Queries: Verify subscription data consistency
API Endpoints
Section titled “API Endpoints”Endpoint | Method | Description |
---|---|---|
/api/webhooks/stripe | POST | Stripe webhook handler |
/api/payments/stripe/portal | POST | Create billing portal session |
Dependencies
Section titled “Dependencies”stripe
- Official Stripe Node.js SDK@stripe/stripe-js
- Stripe.js for frontend integration (if needed)
This comprehensive Stripe implementation provides a robust foundation for subscription management, usage-based billing, and customer self-service in the FloTorch Console application.