Skip to content

Stripe Implementation

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.

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

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
},
},
});
  1. 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
  2. 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
  3. Set up Billing Portal in Stripe Dashboard:

    • Configure allowed features (payment methods, invoices, etc.)
    • Set return URL to your application

The useStripe() composable provides all Stripe API interactions:

server/utils/stripe.ts
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
};
};
// Create a new customer
const 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 one
const customer = await createOrGetCustomer(
"123", // orgId
"user@example.com", // email
"John Doe", // name
{ orgId: "123", orgUid: "uuid", source: "flotorch_console" }
);
// Create subscription with multiple price items
const 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 creation
const { 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" },
],
});
// Report usage for metered billing
const response = await reportUsage(
"cus_xxxxx", // customerId
"additional_requests", // eventName (dimension)
Date.now(), // timestamp
100, // value
"usage-uuid" // unique identifier
);
// Create billing portal session
const portalUrl = await createPortalLink("cus_xxxxx");
// Returns: https://billing.stripe.com/session/xxxxx

The webhook handler (server/api/webhooks/stripe/index.post.ts) processes real-time events from Stripe:

Event TypeDescriptionAction
customer.subscription.createdNew subscription createdLink subscription to organization
customer.subscription.updatedSubscription modifiedUpdate subscription status and end date
customer.subscription.deletedSubscription canceledMark as canceled, set end date
customer.subscription.pausedSubscription pausedUpdate status to paused
customer.subscription.resumedSubscription resumedReactivate subscription
invoice.payment_succeededPayment successfulUpdate subscription status to active
invoice.payment_failedPayment failedUpdate status to past_due
customer.subscription.trial_will_endTrial ending soonUpdate trial status
// Verify webhook signature
const stripeEvent = await verifyWebhookSignature(rawBody, signature);
// Process event based on type
switch (stripeEvent.type) {
case "customer.subscription.created":
await handleSubscriptionCreated(stripeEvent.data.object);
break;
// ... other event handlers
}

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;
}

The Stripe integration uses the following database fields in the orgs table:

server/database/schema/orgs.ts
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
});
shared/types/stripe.schema.ts
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
]);

The system tracks and reports usage for metered billing:

server/utils/metering.ts
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 */ },
};
}
// Report usage to Stripe
export 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()}`
);
}
}

The 03.subscriptions.ts plugin automatically creates Stripe subscriptions for organizations without billing:

// Find orgs without any billing setup
const 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 org
for (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,
});
}

The system handles various subscription states:

// Handle subscription updates
async 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,
});
}

Customers can manage their subscriptions through Stripe’s billing portal:

server/api/payments/stripe/portal.post.ts
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 };
},
});

The implementation includes comprehensive error handling:

// Stripe configuration validation
const 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 verification
try {
stripeEvent = await verifyWebhookSignature(rawBody, signature);
} catch {
throw createError({
statusCode: 400,
statusMessage: "Invalid webhook signature",
});
}
  1. Webhook Signature Verification: All webhooks are verified using Stripe’s signature validation
  2. Environment Variables: Sensitive keys are stored in runtime configuration
  3. Database Encryption: Customer IDs and subscription IDs are stored securely
  4. Access Control: Billing portal access requires organization authentication
  5. Idempotency: Usage reporting uses idempotency keys to prevent duplicate charges

The system includes comprehensive logging for monitoring:

// Log subscription events
console.log(`Updated org ${org.id} with subscription ${subscription.id}, status: ${subscription.status}`);
// Log webhook processing
console.log(`Handling subscription created event for subscription ${stripeEvent.data.object.id}`);
// Log errors with context
console.error("error", "Subscriptions plugin failed:", error);

For testing, use Stripe’s test mode:

// Use test keys
stripe: {
secretKey: "sk_test_xxxxx", // Test secret key
webhookSecret: "whsec_test_xxxxx", // Test webhook secret
// ... other test configuration
}

Use Stripe CLI for local webhook testing:

Terminal window
# Install Stripe CLI
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger customer.subscription.created
stripe trigger invoice.payment_succeeded
  1. Idempotency: Always use idempotency keys for usage reporting
  2. Error Handling: Implement comprehensive error handling for all Stripe operations
  3. Webhook Security: Always verify webhook signatures
  4. Race Conditions: Use retry logic for webhook processing
  5. Monitoring: Log all important events for debugging and monitoring
  6. Testing: Use Stripe’s test mode for development and testing
  7. Data Consistency: Keep local database in sync with Stripe data
  8. Customer Experience: Provide clear billing portal access and error messages
  1. Webhook Signature Verification Failed

    • Check webhook secret configuration
    • Ensure raw body is used for signature verification
    • Verify webhook endpoint URL
  2. Subscription Not Created

    • Check Stripe API keys and permissions
    • Verify price IDs exist in Stripe
    • Check trial days configuration
  3. Usage Not Reported

    • Verify customer has active subscription
    • Check usage dimension names match Stripe configuration
    • Ensure idempotency keys are unique
  4. Billing Portal Access Denied

    • Verify customer ID exists in database
    • Check organization authentication
    • Ensure billing portal is configured in Stripe
  • 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
EndpointMethodDescription
/api/webhooks/stripePOSTStripe webhook handler
/api/payments/stripe/portalPOSTCreate billing portal session
  • 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.