Last updated: Aug 4, 2025, 11:26 AM UTC

Stripe Subscriptions Implementation Guide

Status: Complete Implementation Guide
Version: 1.0
Purpose: Step-by-step procedures for implementing Stripe subscription billing for any SaaS platform
Applicable To: All SaaS applications requiring subscription billing, usage tracking, and quota management


Overview

This guide provides comprehensive procedures for implementing Stripe subscription billing integrated with usage tracking and quota enforcement for any SaaS platform. The approach emphasizes secure payment processing, automated billing cycles, and real-time usage monitoring that can be adapted to any service or product offering.

Key Benefits

  • Automated Billing: Stripe handles recurring subscription charges for any service
  • Secure Payments: PCI-compliant payment processing
  • Usage Tracking: Configurable quota monitoring for any metric
  • Plan Management: Seamless upgrades/downgrades between tiers
  • Webhook Integration: Automated subscription status updates
  • Framework Agnostic: Works with Next.js, Express, Django, Rails, etc.

Initial Stripe Setup

Step 1: Stripe Account Configuration

Create and configure your Stripe account for subscription billing:

# Install Stripe CLI for testing
npm install -g stripe-cli

# Login to Stripe CLI
stripe login

# Create a new product in Stripe Dashboard
# Replace with your service name and description
stripe products create \
  --name "Your SaaS Service" \
  --description "Your service description"

# Create pricing plans (adjust amounts and names for your service)
stripe prices create \
  --product prod_XXXXX \
  --unit-amount 0 \
  --currency usd \
  --recurring interval=month \
  --nickname "Free Plan"

stripe prices create \
  --product prod_XXXXX \
  --unit-amount 2900 \
  --currency usd \
  --recurring interval=month \
  --nickname "Professional Plan"

stripe prices create \
  --product prod_XXXXX \
  --unit-amount 9900 \
  --currency usd \
  --recurring interval=month \
  --nickname "Enterprise Plan"

Step 2: Environment Configuration

# .env.local
STRIPE_SECRET_KEY=sk_test_51xxxxx...
STRIPE_PUBLISHABLE_KEY=pk_test_51xxxxx...
STRIPE_WEBHOOK_SECRET=whsec_xxxxx...

# Stripe Price IDs (from Stripe Dashboard - replace with your actual price IDs)
STRIPE_PRICE_FREE=price_xxxxx
STRIPE_PRICE_PROFESSIONAL=price_xxxxx
STRIPE_PRICE_ENTERPRISE=price_xxxxx

# Webhook endpoint (replace with your domain)
STRIPE_WEBHOOK_ENDPOINT=https://yourdomain.com/api/webhooks/stripe

Step 3: Database Schema for Subscriptions

-- Extend user profiles for subscription data
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS stripe_customer_id TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS stripe_subscription_id TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS subscription_status TEXT DEFAULT 'trial';
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS current_period_start TIMESTAMPTZ;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS current_period_end TIMESTAMPTZ;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS cancel_at_period_end BOOLEAN DEFAULT false;

-- Subscription plans table
CREATE TABLE subscription_plans (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    stripe_price_id TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    amount_cents INTEGER NOT NULL,
    currency TEXT DEFAULT 'usd',
    interval_type TEXT NOT NULL, -- 'month', 'year'
    features JSONB NOT NULL,
    limits JSONB NOT NULL, -- { "conversions": 50, "file_size_mb": 5 }
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Insert plan data (customize features and limits for your service)
INSERT INTO subscription_plans (stripe_price_id, name, amount_cents, interval_type, features, limits) VALUES
('price_free', 'Free', 0, 'month', 
 '{"feature_1": true, "feature_2": false, "support_level": "community"}',
 '{"usage_limit": 100, "storage_gb": 1, "api_calls": 1000}'),
('price_professional', 'Professional', 2900, 'month',
 '{"feature_1": true, "feature_2": true, "support_level": "priority", "analytics": true}',
 '{"usage_limit": 10000, "storage_gb": 50, "api_calls": 100000}'),
('price_enterprise', 'Enterprise', 9900, 'month',
 '{"feature_1": true, "feature_2": true, "support_level": "dedicated", "analytics": true, "custom_integrations": true}',
 '{"usage_limit": -1, "storage_gb": -1, "api_calls": -1}');

-- Usage tracking table (customize metric_type for your service)
CREATE TABLE usage_records (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    metric_type TEXT NOT NULL, -- Examples: 'api_calls', 'storage_used', 'emails_sent', 'reports_generated'
    quantity INTEGER NOT NULL DEFAULT 1,
    metadata JSONB, -- Additional context: file_size, request_type, feature_used, etc.
    period_start TIMESTAMPTZ NOT NULL,
    period_end TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Billing events table
CREATE TABLE billing_events (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    stripe_event_id TEXT UNIQUE NOT NULL,
    event_type TEXT NOT NULL, -- 'invoice.paid', 'subscription.updated', etc.
    processed_at TIMESTAMPTZ DEFAULT NOW(),
    data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE subscription_plans ENABLE ROW LEVEL SECURITY;
ALTER TABLE usage_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing_events ENABLE ROW LEVEL SECURITY;

-- RLS Policies
CREATE POLICY "subscription_plans_public" ON subscription_plans FOR SELECT USING (is_active = true);
CREATE POLICY "usage_records_user_access" ON usage_records FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "billing_events_user_access" ON billing_events FOR ALL USING (auth.uid() = user_id);

Stripe Integration Implementation

Step 1: Stripe Client Configuration

// lib/stripe/config.ts
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
  typescript: true,
})

export const STRIPE_CONFIG = {
  publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
  prices: {
    free: process.env.STRIPE_PRICE_FREE!,
    professional: process.env.STRIPE_PRICE_PROFESSIONAL!,
    enterprise: process.env.STRIPE_PRICE_ENTERPRISE!,
  },
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
}

// Plan configuration - Customize for your service
export const SUBSCRIPTION_PLANS = {
  free: {
    name: 'Free',
    price: 0,
    stripePriceId: STRIPE_CONFIG.prices.free,
    limits: {
      // Customize these limits for your service
      apiCalls: 1000,
      storageGB: 1,
      features: 2,
    },
    features: ['Basic Features', 'Community Support', 'Core Functionality'],
  },
  professional: {
    name: 'Professional',
    price: 29,
    stripePriceId: STRIPE_CONFIG.prices.professional,
    limits: {
      // Customize these limits for your service
      apiCalls: 100000,
      storageGB: 50,
      features: 10,
    },
    features: ['Everything in Free', 'Priority Support', 'Advanced Features', 'Analytics'],
  },
  enterprise: {
    name: 'Enterprise',
    price: 99,
    stripePriceId: STRIPE_CONFIG.prices.enterprise,
    limits: {
      // -1 means unlimited
      apiCalls: -1,
      storageGB: -1,
      features: -1,
    },
    features: ['Everything in Professional', 'Dedicated Support', 'Custom Integrations', 'SLA'],
  },
} as const

export type PlanType = keyof typeof SUBSCRIPTION_PLANS

Step 2: Customer Management Service

// lib/stripe/customer-service.ts
import { stripe } from './config'
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export class StripeCustomerService {
  private supabase = createServerComponentClient({ cookies })

  async createCustomer(userId: string, email: string, name?: string): Promise<string> {
    try {
      // Check if customer already exists
      const { data: profile } = await this.supabase
        .from('user_profiles')
        .select('stripe_customer_id')
        .eq('user_id', userId)
        .single()

      if (profile?.stripe_customer_id) {
        return profile.stripe_customer_id
      }

      // Create new Stripe customer
      const customer = await stripe.customers.create({
        email,
        name,
        metadata: {
          user_id: userId,
        },
      })

      // Store customer ID in database
      await this.supabase
        .from('user_profiles')
        .update({ stripe_customer_id: customer.id })
        .eq('user_id', userId)

      return customer.id
    } catch (error) {
      console.error('Error creating Stripe customer:', error)
      throw new Error('Failed to create customer')
    }
  }

  async getCustomer(customerId: string): Promise<Stripe.Customer> {
    return await stripe.customers.retrieve(customerId) as Stripe.Customer
  }

  async updateCustomer(customerId: string, updates: Stripe.CustomerUpdateParams): Promise<Stripe.Customer> {
    return await stripe.customers.update(customerId, updates)
  }

  async getCustomerByUserId(userId: string): Promise<string | null> {
    const { data: profile } = await this.supabase
      .from('user_profiles')
      .select('stripe_customer_id')
      .eq('user_id', userId)
      .single()

    return profile?.stripe_customer_id || null
  }
}

export const customerService = new StripeCustomerService()

Step 3: Subscription Management Service

// lib/stripe/subscription-service.ts
import { stripe, SUBSCRIPTION_PLANS, type PlanType } from './config'
import { customerService } from './customer-service'
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export class StripeSubscriptionService {
  private supabase = createServerComponentClient({ cookies })

  async createCheckoutSession(
    userId: string,
    email: string,
    planType: PlanType,
    successUrl: string,
    cancelUrl: string
  ): Promise<string> {
    try {
      // Ensure customer exists
      const customerId = await customerService.createCustomer(userId, email)
      
      const plan = SUBSCRIPTION_PLANS[planType]
      
      // Create checkout session
      const session = await stripe.checkout.sessions.create({
        customer: customerId,
        payment_method_types: ['card'],
        line_items: [
          {
            price: plan.stripePriceId,
            quantity: 1,
          },
        ],
        mode: 'subscription',
        success_url: successUrl,
        cancel_url: cancelUrl,
        metadata: {
          user_id: userId,
          plan_type: planType,
        },
        subscription_data: {
          metadata: {
            user_id: userId,
            plan_type: planType,
          },
        },
      })

      return session.url!
    } catch (error) {
      console.error('Error creating checkout session:', error)
      throw new Error('Failed to create checkout session')
    }
  }

  async createBillingPortalSession(customerId: string, returnUrl: string): Promise<string> {
    try {
      const session = await stripe.billingPortal.sessions.create({
        customer: customerId,
        return_url: returnUrl,
      })

      return session.url
    } catch (error) {
      console.error('Error creating billing portal session:', error)
      throw new Error('Failed to create billing portal session')
    }
  }

  async getSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
    return await stripe.subscriptions.retrieve(subscriptionId)
  }

  async cancelSubscription(subscriptionId: string, cancelAtPeriodEnd: boolean = true): Promise<Stripe.Subscription> {
    return await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: cancelAtPeriodEnd,
    })
  }

  async updateSubscription(subscriptionId: string, newPriceId: string): Promise<Stripe.Subscription> {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId)
    
    return await stripe.subscriptions.update(subscriptionId, {
      items: [
        {
          id: subscription.items.data[0].id,
          price: newPriceId,
        },
      ],
      proration_behavior: 'create_prorations',
    })
  }

  async syncSubscriptionToDatabase(subscription: Stripe.Subscription): Promise<void> {
    const userId = subscription.metadata.user_id
    
    if (!userId) {
      console.error('No user_id in subscription metadata')
      return
    }

    const updates = {
      stripe_subscription_id: subscription.id,
      subscription_status: subscription.status,
      current_period_start: new Date(subscription.current_period_start * 1000).toISOString(),
      current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
      cancel_at_period_end: subscription.cancel_at_period_end,
      plan_type: subscription.metadata.plan_type || 'free',
      updated_at: new Date().toISOString(),
    }

    await this.supabase
      .from('user_profiles')
      .update(updates)
      .eq('user_id', userId)
  }
}

export const subscriptionService = new StripeSubscriptionService()

API Routes Implementation

Step 1: Checkout Session Creation

// app/api/stripe/create-checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { subscriptionService } from '@/lib/stripe/subscription-service'
import { SUBSCRIPTION_PLANS, type PlanType } from '@/lib/stripe/config'

export async function POST(request: NextRequest) {
  try {
    const supabase = createRouteHandlerClient({ cookies })
    
    // Authenticate user
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const { planType } = await request.json()
    
    // Validate plan type
    if (!planType || !(planType in SUBSCRIPTION_PLANS)) {
      return NextResponse.json({ error: 'Invalid plan type' }, { status: 400 })
    }

    const plan = SUBSCRIPTION_PLANS[planType as PlanType]
    
    // Free plan doesn't need checkout
    if (planType === 'free') {
      // Update user to free plan directly
      await supabase
        .from('user_profiles')
        .update({ 
          plan_type: 'free',
          subscription_status: 'active',
          updated_at: new Date().toISOString()
        })
        .eq('user_id', session.user.id)
      
      return NextResponse.json({ 
        success: true, 
        message: 'Switched to free plan' 
      })
    }

    // Create checkout session for paid plans
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
    const successUrl = `${baseUrl}/dashboard/billing/success?session_id={CHECKOUT_SESSION_ID}`
    const cancelUrl = `${baseUrl}/dashboard/billing`

    const checkoutUrl = await subscriptionService.createCheckoutSession(
      session.user.id,
      session.user.email!,
      planType as PlanType,
      successUrl,
      cancelUrl
    )

    return NextResponse.json({ checkoutUrl })
  } catch (error: any) {
    console.error('Checkout creation error:', error)
    return NextResponse.json(
      { error: 'Failed to create checkout session' },
      { status: 500 }
    )
  }
}

Step 2: Billing Portal Access

// app/api/stripe/billing-portal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { customerService } from '@/lib/stripe/customer-service'
import { subscriptionService } from '@/lib/stripe/subscription-service'

export async function POST(request: NextRequest) {
  try {
    const supabase = createRouteHandlerClient({ cookies })
    
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Get customer ID
    const customerId = await customerService.getCustomerByUserId(session.user.id)
    if (!customerId) {
      return NextResponse.json({ error: 'No customer found' }, { status: 404 })
    }

    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
    const returnUrl = `${baseUrl}/dashboard/billing`

    const portalUrl = await subscriptionService.createBillingPortalSession(
      customerId,
      returnUrl
    )

    return NextResponse.json({ portalUrl })
  } catch (error: any) {
    console.error('Billing portal error:', error)
    return NextResponse.json(
      { error: 'Failed to create billing portal session' },
      { status: 500 }
    )
  }
}

Step 3: Webhook Handler

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { headers } from 'next/headers'
import { stripe, STRIPE_CONFIG } from '@/lib/stripe/config'
import { subscriptionService } from '@/lib/stripe/subscription-service'
import { createServiceRoleClient } from '@supabase/auth-helpers-nextjs'
import Stripe from 'stripe'

// Use service role client for webhook processing
const supabase = createServiceRoleClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(request: NextRequest) {
  try {
    const body = await request.text()
    const headersList = headers()
    const signature = headersList.get('stripe-signature')

    if (!signature) {
      return NextResponse.json({ error: 'No signature' }, { status: 400 })
    }

    // Verify webhook signature
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      STRIPE_CONFIG.webhookSecret
    )

    console.log(`Received Stripe webhook: ${event.type}`)

    // Handle different event types
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)
        break

      case 'customer.subscription.created':
      case 'customer.subscription.updated':
        await handleSubscriptionChange(event.data.object as Stripe.Subscription)
        break

      case 'customer.subscription.deleted':
        await handleSubscriptionCanceled(event.data.object as Stripe.Subscription)
        break

      case 'invoice.payment_succeeded':
        await handlePaymentSucceeded(event.data.object as Stripe.Invoice)
        break

      case 'invoice.payment_failed':
        await handlePaymentFailed(event.data.object as Stripe.Invoice)
        break

      default:
        console.log(`Unhandled event type: ${event.type}`)
    }

    // Log the event
    await logBillingEvent(event)

    return NextResponse.json({ received: true })
  } catch (error: any) {
    console.error('Webhook error:', error.message)
    return NextResponse.json(
      { error: 'Webhook processing failed' },
      { status: 400 }
    )
  }
}

async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
  console.log('Checkout completed:', session.id)
  
  if (session.subscription) {
    const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
    await subscriptionService.syncSubscriptionToDatabase(subscription)
  }
}

async function handleSubscriptionChange(subscription: Stripe.Subscription) {
  console.log('Subscription updated:', subscription.id)
  await subscriptionService.syncSubscriptionToDatabase(subscription)
}

async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
  console.log('Subscription canceled:', subscription.id)
  
  const userId = subscription.metadata.user_id
  if (userId) {
    // Move user to free plan
    await supabase
      .from('user_profiles')
      .update({ 
        plan_type: 'free',
        subscription_status: 'canceled',
        stripe_subscription_id: null,
        updated_at: new Date().toISOString()
      })
      .eq('user_id', userId)
  }
}

async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
  console.log('Payment succeeded:', invoice.id)
  
  if (invoice.subscription) {
    const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
    await subscriptionService.syncSubscriptionToDatabase(subscription)
  }
}

async function handlePaymentFailed(invoice: Stripe.Invoice) {
  console.log('Payment failed:', invoice.id)
  
  // Handle failed payment logic
  // Could send email notification, update subscription status, etc.
}

async function logBillingEvent(event: Stripe.Event) {
  try {
    let userId = null
    
    // Extract user ID from various event types
    if (event.data.object && typeof event.data.object === 'object') {
      const obj = event.data.object as any
      userId = obj.metadata?.user_id || 
               obj.customer?.metadata?.user_id ||
               obj.subscription?.metadata?.user_id
    }

    await supabase
      .from('billing_events')
      .insert({
        user_id: userId,
        stripe_event_id: event.id,
        event_type: event.type,
        data: event.data.object,
        processed_at: new Date().toISOString()
      })
  } catch (error) {
    console.error('Error logging billing event:', error)
  }
}

Usage Tracking and Quota Enforcement

Step 1: Usage Tracking Service

// lib/usage/usage-tracker.ts
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { SUBSCRIPTION_PLANS, type PlanType } from '@/lib/stripe/config'

export class UsageTracker {
  private supabase = createServerComponentClient({ cookies })

  async recordUsage(
    userId: string,
    metricType: string, // Examples: 'api_calls', 'storage_used', 'emails_sent', 'reports_generated'
    quantity: number = 1,
    metadata?: Record<string, any>
  ): Promise<void> {
    const now = new Date()
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1)
    const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)

    try {
      await this.supabase
        .from('usage_records')
        .insert({
          user_id: userId,
          metric_type: metricType,
          quantity,
          metadata,
          period_start: periodStart.toISOString(),
          period_end: periodEnd.toISOString()
        })
    } catch (error) {
      console.error('Error recording usage:', error)
    }
  }

  async getCurrentUsage(userId: string, metricType: string): Promise<number> {
    const now = new Date()
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1)

    const { data, error } = await this.supabase
      .from('usage_records')
      .select('quantity')
      .eq('user_id', userId)
      .eq('metric_type', metricType)
      .gte('created_at', periodStart.toISOString())

    if (error) {
      console.error('Error fetching usage:', error)
      return 0
    }

    return data?.reduce((sum, record) => sum + record.quantity, 0) || 0
  }

  async checkQuota(userId: string, metricType: string, requestedQuantity: number = 1): Promise<{
    allowed: boolean
    used: number
    limit: number
    remaining: number
  }> {
    // Get user's plan
    const { data: profile } = await this.supabase
      .from('user_profiles')
      .select('plan_type, subscription_status')
      .eq('user_id', userId)
      .single()

    if (!profile) {
      throw new Error('User profile not found')
    }

    // Get plan limits
    const planType = (profile.plan_type || 'free') as PlanType
    const plan = SUBSCRIPTION_PLANS[planType]
    const limit = plan.limits[metricType as keyof typeof plan.limits] as number

    // Unlimited quota
    if (limit === -1) {
      return {
        allowed: true,
        used: 0,
        limit: -1,
        remaining: -1
      }
    }

    // Get current usage
    const used = await this.getCurrentUsage(userId, metricType)
    const remaining = Math.max(0, limit - used)
    const allowed = (used + requestedQuantity) <= limit

    return {
      allowed,
      used,
      limit,
      remaining
    }
  }

  async getUserUsageSummary(userId: string): Promise<{
    plan: string
    period: { start: Date; end: Date }
    usage: Record<string, { used: number; limit: number; percentage: number }>
  }> {
    const { data: profile } = await this.supabase
      .from('user_profiles')
      .select('plan_type')
      .eq('user_id', userId)
      .single()

    const planType = (profile?.plan_type || 'free') as PlanType
    const plan = SUBSCRIPTION_PLANS[planType]

    const now = new Date()
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1)
    const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0)

    const usage: Record<string, { used: number; limit: number; percentage: number }> = {}

    for (const [metric, limit] of Object.entries(plan.limits)) {
      const used = await this.getCurrentUsage(userId, metric)
      const percentage = limit === -1 ? 0 : Math.round((used / limit) * 100)

      usage[metric] = {
        used,
        limit: limit as number,
        percentage
      }
    }

    return {
      plan: planType,
      period: { start: periodStart, end: periodEnd },
      usage
    }
  }
}

export const usageTracker = new UsageTracker()

Step 2: Quota Enforcement Middleware

// lib/middleware/quota-middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { usageTracker } from '@/lib/usage/usage-tracker'

export async function withQuotaCheck(
  request: NextRequest,
  metricType: string = 'api_calls', // Default metric - customize for your service
  quantity: number = 1
) {
  const response = NextResponse.next()
  const supabase = createMiddlewareClient({ req: request, res: response })

  try {
    // Get authenticated user
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json(
        { error: { code: 'AUTH_001', message: 'Authentication required' } },
        { status: 401 }
      )
    }

    // Check quota
    const quotaCheck = await usageTracker.checkQuota(
      session.user.id,
      metricType,
      quantity
    )

    if (!quotaCheck.allowed) {
      return NextResponse.json({
        error: {
          code: 'QUOTA_001',
          message: `${metricType.replace('_', ' ')} quota exceeded`,
          details: {
            used: quotaCheck.used,
            limit: quotaCheck.limit,
            requested: quantity
          }
        },
        help: 'Upgrade your plan for higher limits',
        upgrade_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard/billing`
      }, { status: 429 })
    }

    // Store quota info in headers for the actual handler
    response.headers.set('x-quota-used', quotaCheck.used.toString())
    response.headers.set('x-quota-limit', quotaCheck.limit.toString())
    response.headers.set('x-quota-remaining', quotaCheck.remaining.toString())

    return response
  } catch (error) {
    console.error('Quota check error:', error)
    return NextResponse.json(
      { error: { code: 'QUOTA_002', message: 'Quota check failed' } },
      { status: 500 }
    )
  }
}

Step 3: Usage API Endpoints

// app/api/usage/summary/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { usageTracker } from '@/lib/usage/usage-tracker'

export async function GET(request: NextRequest) {
  try {
    const supabase = createRouteHandlerClient({ cookies })
    
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const summary = await usageTracker.getUserUsageSummary(session.user.id)

    return NextResponse.json({
      period: {
        start: summary.period.start.toISOString(),
        end: summary.period.end.toISOString(),
        timezone: 'UTC'
      },
      plan: {
        type: summary.plan,
        name: summary.plan.charAt(0).toUpperCase() + summary.plan.slice(1)
      },
      usage: summary.usage
    })
  } catch (error: any) {
    console.error('Usage summary error:', error)
    return NextResponse.json(
      { error: 'Failed to get usage summary' },
      { status: 500 }
    )
  }
}

Frontend Components

Step 1: Plan Selection Component

// components/billing/PlanSelector.tsx
'use client'
import { useState } from 'react'
import { SUBSCRIPTION_PLANS, type PlanType } from '@/lib/stripe/config'

interface Plan {
  type: PlanType
  name: string
  price: number
  features: string[]
  limits: {
    // Customize these limit types for your service
    apiCalls: number
    storageGB: number
    features: number
  }
  popular?: boolean
}

// Customize these plans for your service
const plans: Plan[] = [
  {
    type: 'free',
    name: 'Free',
    price: 0,
    features: ['Basic Features', 'Community Support', 'Core Functionality'],
    limits: SUBSCRIPTION_PLANS.free.limits,
  },
  {
    type: 'professional',
    name: 'Professional',
    price: 29,
    features: ['Everything in Free', 'Priority Support', 'Advanced Features', 'Analytics'],
    limits: SUBSCRIPTION_PLANS.professional.limits,
    popular: true,
  },
  {
    type: 'enterprise',
    name: 'Enterprise',
    price: 99,
    features: ['Everything in Professional', 'Dedicated Support', 'Custom Integrations', 'SLA'],
    limits: SUBSCRIPTION_PLANS.enterprise.limits,
  },
]

interface PlanSelectorProps {
  currentPlan?: PlanType
  onPlanSelect: (planType: PlanType) => void
}

export function PlanSelector({ currentPlan = 'free', onPlanSelect }: PlanSelectorProps) {
  const [loading, setLoading] = useState<PlanType | null>(null)

  const handlePlanSelect = async (planType: PlanType) => {
    if (planType === currentPlan || loading) return
    
    setLoading(planType)
    try {
      await onPlanSelect(planType)
    } finally {
      setLoading(null)
    }
  }

  const formatLimit = (value: number, unit: string) => {
    if (value === -1) return 'Unlimited'
    if (value >= 1000) return `${(value / 1000).toFixed(0)}K ${unit}`
    return `${value} ${unit}`
  }

  return (
    <div className="space-y-8">
      <div className="text-center">
        <h2 className="text-3xl font-bold text-gray-900">Choose Your Plan</h2>
        <p className="mt-2 text-gray-600">
          Start free, upgrade as you grow
        </p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
        {plans.map((plan) => (
          <div
            key={plan.type}
            className={`relative rounded-xl border-2 p-8 ${
              plan.popular
                ? 'border-blue-500 shadow-lg scale-105'
                : 'border-gray-200'
            } ${
              currentPlan === plan.type ? 'ring-2 ring-green-500' : ''
            }`}
          >
            {plan.popular && (
              <div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
                <span className="bg-blue-500 text-white px-4 py-1 text-sm rounded-full">
                  Most Popular
                </span>
              </div>
            )}

            <div className="text-center">
              <h3 className="text-2xl font-bold text-gray-900">{plan.name}</h3>
              <div className="mt-4">
                <span className="text-4xl font-bold text-gray-900">
                  ${plan.price}
                </span>
                {plan.price > 0 && (
                  <span className="text-gray-500">/month</span>
                )}
              </div>
            </div>

            <div className="mt-6 space-y-4">
              <div className="text-center text-sm text-gray-600">
                <div>
                  {formatLimit(plan.limits.apiCalls, 'API calls/month')}
                </div>
                <div>
                  {formatLimit(plan.limits.storageGB, 'GB storage')}
                </div>
                <div>
                  {formatLimit(plan.limits.features, 'features')}
                </div>
              </div>

              <ul className="space-y-3">
                {plan.features.map((feature, index) => (
                  <li key={index} className="flex items-center text-sm">
                    <div className="w-4 h-4 bg-green-500 rounded-full mr-3 flex-shrink-0">
                      <svg className="w-3 h-3 text-white ml-0.5 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
                        <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
                      </svg>
                    </div>
                    <span className="text-gray-700">{feature}</span>
                  </li>
                ))}
              </ul>
            </div>

            <div className="mt-8">
              {currentPlan === plan.type ? (
                <div className="text-center py-3 text-green-600 font-semibold">
                  Current Plan
                </div>
              ) : (
                <button
                  onClick={() => handlePlanSelect(plan.type)}
                  disabled={loading === plan.type}
                  className={`w-full py-3 px-4 rounded-lg font-semibold transition-colors ${
                    plan.popular
                      ? 'bg-blue-600 text-white hover:bg-blue-700'
                      : 'border-2 border-gray-300 text-gray-700 hover:border-gray-400'
                  } disabled:opacity-50 disabled:cursor-not-allowed`}
                >
                  {loading === plan.type ? (
                    <div className="flex items-center justify-center">
                      <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current mr-2"></div>
                      Processing...
                    </div>
                  ) : (
                    plan.type === 'free' ? 'Switch to Free' : `Upgrade to ${plan.name}`
                  )}
                </button>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Step 2: Usage Dashboard Component

// components/billing/UsageDashboard.tsx
'use client'
import { useState, useEffect } from 'react'

interface UsageData {
  period: {
    start: string
    end: string
  }
  plan: {
    type: string
    name: string
  }
  usage: Record<string, {
    used: number
    limit: number
    percentage: number
  }>
}

export function UsageDashboard() {
  const [usage, setUsage] = useState<UsageData | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchUsage()
  }, [])

  const fetchUsage = async () => {
    try {
      const response = await fetch('/api/usage/summary')
      if (response.ok) {
        const data = await response.json()
        setUsage(data)
      }
    } catch (error) {
      console.error('Error fetching usage:', error)
    } finally {
      setLoading(false)
    }
  }

  const formatLimit = (limit: number) => {
    if (limit === -1) return 'Unlimited'
    if (limit >= 1000) return `${(limit / 1000).toFixed(0)}K`
    return limit.toString()
  }

  if (loading) {
    return (
      <div className="space-y-6">
        <div className="animate-pulse bg-gray-200 rounded-lg h-32"></div>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          {[...Array(3)].map((_, i) => (
            <div key={i} className="animate-pulse bg-gray-200 rounded-lg h-32"></div>
          ))}
        </div>
      </div>
    )
  }

  if (!usage) {
    return (
      <div className="text-center py-8">
        <p className="text-gray-500">Unable to load usage data</p>
      </div>
    )
  }

  const periodStart = new Date(usage.period.start).toLocaleDateString()
  const periodEnd = new Date(usage.period.end).toLocaleDateString()

  return (
    <div className="space-y-6">
      <div className="bg-white rounded-lg shadow p-6">
        <div className="flex items-center justify-between">
          <div>
            <h3 className="text-lg font-semibold text-gray-900">
              Current Plan: {usage.plan.name}
            </h3>
            <p className="text-sm text-gray-500">
              Billing period: {periodStart} - {periodEnd}
            </p>
          </div>
          <button className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
            Manage Billing
          </button>
        </div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {Object.entries(usage.usage).map(([metric, data]) => (
          <div key={metric} className="bg-white rounded-lg shadow p-6">
            <div className="flex items-center justify-between mb-4">
              <h4 className="text-sm font-medium text-gray-700 capitalize">
                {metric.replace('_', ' ')}
              </h4>
              <span className="text-2xl font-bold text-gray-900">
                {data.used.toLocaleString()}
              </span>
            </div>

            <div className="text-sm text-gray-500 mb-3">
              of {formatLimit(data.limit)} 
              {data.limit !== -1 && ` (${data.percentage}%)`}
            </div>

            {data.limit !== -1 && (
              <div className="w-full bg-gray-200 rounded-full h-2">
                <div
                  className={`h-2 rounded-full ${
                    data.percentage >= 90 ? 'bg-red-500' :
                    data.percentage >= 75 ? 'bg-yellow-500' :
                    'bg-green-500'
                  }`}
                  style={{ width: `${Math.min(data.percentage, 100)}%` }}
                ></div>
              </div>
            )}

            {data.percentage >= 90 && data.limit !== -1 && (
              <div className="mt-2 text-sm text-red-600">
                ⚠️ Approaching limit
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  )
}

Integration with Your Service API

Step 1: API Endpoint with Quota Check Example

// app/api/v1/your-service-endpoint/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { usageTracker } from '@/lib/usage/usage-tracker'
import { yourServiceFunction } from '@/lib/services/your-service'

export async function POST(request: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })

  try {
    // Authenticate user
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({
        error: {
          code: 'AUTH_001',
          message: 'Authentication required'
        }
      }, { status: 401 })
    }

    // Check your service quota (customize metric name)
    const quotaCheck = await usageTracker.checkQuota(
      session.user.id,
      'api_calls', // Or 'reports_generated', 'emails_sent', etc.
      1
    )

    if (!quotaCheck.allowed) {
      return NextResponse.json({
        error: {
          code: 'QUOTA_001',
          message: 'Monthly API quota exceeded',
          details: {
            used: quotaCheck.used,
            limit: quotaCheck.limit,
            remaining: quotaCheck.remaining
          }
        },
        help: 'Upgrade your plan for higher limits',
        upgrade_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard/billing`
      }, { status: 429 })
    }

    // Get request data
    const requestData = await request.json()

    // Check plan-specific limits (example: data size, complexity, etc.)
    const { data: profile } = await supabase
      .from('user_profiles')
      .select('plan_type')
      .eq('user_id', session.user.id)
      .single()

    const planType = profile?.plan_type || 'free'
    
    // Example: Check data size limits based on plan
    if (requestData.size && requestData.size > getPlanLimit(planType, 'maxDataSize')) {
      return NextResponse.json({
        error: {
          code: 'VAL_002',
          message: `Data size too large for ${planType} plan`,
          details: {
            dataSize: requestData.size,
            maxSize: getPlanLimit(planType, 'maxDataSize'),
            plan: planType
          }
        },
        help: `Upgrade your plan for larger data limits`,
        upgrade_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard/billing`
      }, { status: 413 })
    }

    // Process the request
    const startTime = Date.now()
    const result = await yourServiceFunction(requestData)
    const processingTime = Date.now() - startTime

    // Record usage
    await usageTracker.recordUsage(session.user.id, 'api_calls', 1, {
      requestSize: JSON.stringify(requestData).length,
      processingTime: `${processingTime}ms`,
      operationType: 'your-service-operation'
    })

    // Return result with quota info
    return NextResponse.json({
      ...result,
      quota: {
        used: quotaCheck.used + 1,
        limit: quotaCheck.limit,
        remaining: quotaCheck.remaining - 1
      }
    }, {
      headers: {
        'X-RateLimit-Used': (quotaCheck.used + 1).toString(),
        'X-RateLimit-Limit': quotaCheck.limit.toString(),
        'X-RateLimit-Remaining': (quotaCheck.remaining - 1).toString()
      }
    })

  } catch (error: any) {
    console.error('Service error:', error)
    return NextResponse.json({
      error: {
        code: 'PROC_001',
        message: 'Service operation failed',
        details: error.message
      }
    }, { status: 500 })
  }
}

// Helper function to get plan limits
function getPlanLimit(planType: string, limitType: string): number {
  const limits = {
    free: { maxDataSize: 1024 * 1024 }, // 1MB
    professional: { maxDataSize: 10 * 1024 * 1024 }, // 10MB
    enterprise: { maxDataSize: 100 * 1024 * 1024 }, // 100MB
  }
  
  return limits[planType as keyof typeof limits]?.[limitType as keyof typeof limits.free] || 0
}

Testing Strategy

Step 1: Stripe Webhook Testing

# Terminal 1: Start your Next.js app
npm run dev

# Terminal 2: Forward webhooks to local endpoint
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Terminal 3: Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_succeeded

Step 2: Integration Tests

// tests/stripe-integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
import { stripe } from '@/lib/stripe/config'
import { subscriptionService } from '@/lib/stripe/subscription-service'

describe('Stripe Integration', () => {
  let testCustomerId: string
  let testSubscriptionId: string

  beforeAll(async () => {
    // Create test customer
    const customer = await stripe.customers.create({
      email: 'test@example.com',
      name: 'Test User',
    })
    testCustomerId = customer.id
  })

  afterAll(async () => {
    // Cleanup test data
    if (testSubscriptionId) {
      await stripe.subscriptions.del(testSubscriptionId)
    }
    await stripe.customers.del(testCustomerId)
  })

  it('should create checkout session for professional plan', async () => {
    const checkoutUrl = await subscriptionService.createCheckoutSession(
      'test-user-id',
      'test@example.com',
      'professional',
      'https://example.com/success',
      'https://example.com/cancel'
    )

    expect(checkoutUrl).toContain('checkout.stripe.com')
  })

  it('should handle subscription creation webhook', async () => {
    // Create test subscription
    const subscription = await stripe.subscriptions.create({
      customer: testCustomerId,
      items: [{ price: process.env.STRIPE_PRICE_PROFESSIONAL }],
      metadata: {
        user_id: 'test-user-id',
        plan_type: 'professional'
      }
    })
    
    testSubscriptionId = subscription.id

    // Test webhook handler
    await subscriptionService.syncSubscriptionToDatabase(subscription)
    
    // Verify database was updated
    // (This would require test database setup)
  })
})

Step 3: Usage Quota Tests

// tests/usage-tracking.test.ts
import { describe, it, expect } from '@jest/globals'
import { usageTracker } from '@/lib/usage/usage-tracker'

describe('Usage Tracking', () => {
  it('should enforce API call limits for free plan', async () => {
    const userId = 'test-free-user'
    
    // Mock user as free plan with quota used
    const quotaCheck = await usageTracker.checkQuota(userId, 'api_calls', 1)
    
    if (quotaCheck.used >= 1000) { // Free plan limit
      expect(quotaCheck.allowed).toBe(false)
    } else {
      expect(quotaCheck.allowed).toBe(true)
    }
  })

  it('should allow unlimited API calls for enterprise plan', async () => {
    const userId = 'test-enterprise-user'
    
    const quotaCheck = await usageTracker.checkQuota(userId, 'api_calls', 10000)
    
    expect(quotaCheck.allowed).toBe(true)
    expect(quotaCheck.limit).toBe(-1)
  })
})

Monitoring and Analytics

Step 1: Stripe Dashboard Monitoring

Set up monitoring for key metrics in Stripe Dashboard:

  • MRR (Monthly Recurring Revenue): Track subscription revenue
  • Churn Rate: Monitor subscription cancellations
  • Conversion Rate: Checkout session to subscription conversion
  • Failed Payments: Track payment failures and retries

Step 2: Usage Analytics

// lib/analytics/usage-analytics.ts
export class UsageAnalytics {
  async getUsageMetrics(startDate: Date, endDate: Date): Promise<{
    totalRequests: number
    requestsByPlan: Record<string, number>
    averageRequestSize: number
    popularOperationTypes: Array<{ type: string; count: number }>
  }> {
    // Implementation would query usage_records table
    // Return aggregated metrics for dashboard
  }

  async getRevenueMetrics(): Promise<{
    mrr: number
    arr: number
    churnRate: number
    upgrades: number
    downgrades: number
  }> {
    // Implementation would query Stripe API and billing_events
    // Return revenue analytics
  }
}

Implementation Checklist

Stripe Setup

  • Create Stripe account and configure products/prices
  • Set up webhook endpoints and verify signatures
  • Configure environment variables
  • Test webhook forwarding with Stripe CLI

Database Schema

  • Create subscription_plans table with plan configurations
  • Add Stripe fields to user_profiles table
  • Create usage_records table for quota tracking
  • Create billing_events table for audit logging
  • Set up proper RLS policies

Backend Implementation

  • Implement StripeCustomerService for customer management
  • Implement StripeSubscriptionService for subscription management
  • Create UsageTracker for quota enforcement
  • Build API routes for checkout and billing portal
  • Set up comprehensive webhook handler

Frontend Components

  • Build PlanSelector component for plan upgrades
  • Create UsageDashboard for quota monitoring
  • Implement billing management interface
  • Add quota warnings and upgrade prompts

API Integration

  • Add quota checks to all service endpoints
  • Record usage for billing and analytics
  • Return quota information in API responses
  • Implement proper error messages for quota exceeded

Testing & Monitoring

  • Set up webhook testing with Stripe CLI
  • Create integration tests for subscription flows
  • Implement usage quota testing
  • Configure Stripe Dashboard monitoring
  • Set up alerts for payment failures and high usage

Customization Guide

Adapting for Your Service

This guide provides a complete framework that can be adapted for any SaaS service. Here are the key areas to customize:

1. Plan Configuration

  • Modify SUBSCRIPTION_PLANS with your service-specific limits
  • Update feature lists to match your offerings
  • Adjust pricing based on your business model

2. Usage Metrics

  • Replace 'api_calls' with metrics relevant to your service:
    • 'emails_sent' for email marketing platforms
    • 'reports_generated' for analytics services
    • 'storage_used' for file storage services
    • 'users_managed' for team management tools

3. Database Schema

  • Customize the features and limits JSONB fields in subscription_plans
  • Add service-specific fields to usage_records metadata
  • Modify plan limits based on your service constraints

4. Quota Enforcement

  • Implement checks specific to your service operations
  • Add custom validation for plan-specific features
  • Create service-appropriate error messages

5. Frontend Components

  • Update plan descriptions and feature lists
  • Modify usage dashboard metrics display
  • Customize upgrade prompts and messaging

Common Use Cases

Email Marketing Platform

// Usage metrics: emails_sent, contacts_stored, campaigns_created
// Plan limits: monthly_emails, contact_count, automation_count

Analytics Dashboard

// Usage metrics: reports_generated, data_processed, api_requests
// Plan limits: monthly_reports, data_retention_days, custom_dashboards

πŸ—„οΈ File Storage Service

// Usage metrics: storage_used, files_uploaded, bandwidth_used
// Plan limits: storage_gb, monthly_uploads, download_bandwidth

Team Management Tool

// Usage metrics: users_managed, projects_created, integrations_used
// Plan limits: team_size, project_count, integration_count

This comprehensive Stripe subscriptions implementation guide provides a complete, customizable framework for integrating subscription billing with usage-based quotas into any SaaS platform. The modular design allows you to adapt all components to your specific service requirements while maintaining robust billing and quota management.