Stripe for SaaS: Complete Integration Guide for Subscriptions

Why Stripe

Stripe dominates SaaS payments because it handles everything:

Alternatives (Paddle, Lemon Squeezy) are merchant-of-record, handling tax compliance but with higher fees. Use them if tax compliance is a major concern.

Core Concepts

Products & Prices

Product: Your plan (e.g., "Pro Plan")
Price: The amount and interval (e.g., "$20/month")

One product can have multiple prices (monthly, yearly, different currencies).

Customers

Stripe Customer = your user/organization. Link Stripe customer ID to your database.

Subscriptions

Ongoing billing relationship. Has a status (active, past_due, canceled) and current period.

Checkout Sessions

Hosted payment page. Redirect users to Stripe to complete payment.

Webhooks

Events Stripe sends to your server (payment succeeded, subscription canceled, etc.).

Implementation Steps

1. Create Products in Stripe Dashboard

Go to Products → Add Product. Create your plans with prices.

Example setup:

2. Create Checkout Session (Backend)

// API route: /api/checkout
const session = await stripe.checkout.sessions.create({
  customer: stripeCustomerId, // or customer_email
  mode: 'subscription',
  line_items: [{
    price: 'price_xxx', // Your Stripe price ID
    quantity: 1,
  }],
  success_url: 'https://yourapp.com/billing?success=true',
  cancel_url: 'https://yourapp.com/pricing',
});

return { url: session.url };

3. Redirect to Checkout (Frontend)

const response = await fetch('/api/checkout', { method: 'POST' });
const { url } = await response.json();
window.location.href = url;

4. Handle Webhooks

// API route: /api/webhooks/stripe
const event = stripe.webhooks.constructEvent(
  body, sig, process.env.STRIPE_WEBHOOK_SECRET
);

switch (event.type) {
  case 'checkout.session.completed':
    // Create subscription in your database
    break;
  case 'customer.subscription.updated':
    // Update subscription status
    break;
  case 'customer.subscription.deleted':
    // Handle cancellation
    break;
  case 'invoice.payment_failed':
    // Notify user, restrict access
    break;
}

Database Schema

CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID REFERENCES organizations(id),
  stripe_customer_id TEXT NOT NULL,
  stripe_subscription_id TEXT UNIQUE,
  status TEXT NOT NULL, -- 'active', 'past_due', 'canceled'
  plan TEXT NOT NULL,   -- 'starter', 'pro'
  current_period_start TIMESTAMPTZ,
  current_period_end TIMESTAMPTZ,
  cancel_at_period_end BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Always store Stripe IDs. You'll need them for API calls and debugging.

Customer Portal

Stripe Customer Portal handles subscription management UI for free:

// API route: /api/billing-portal
const session = await stripe.billingPortal.sessions.create({
  customer: stripeCustomerId,
  return_url: 'https://yourapp.com/settings',
});

return { url: session.url };

Configure the portal in Stripe Dashboard → Settings → Customer Portal.

Common Patterns

Free Trial

const session = await stripe.checkout.sessions.create({
  // ...
  subscription_data: {
    trial_period_days: 14,
  },
});

Feature Gating

// Check plan before allowing feature
function canUseFeature(subscription, feature) {
  const planFeatures = {
    starter: ['basic_feature'],
    pro: ['basic_feature', 'advanced_feature', 'api_access'],
  };
  return planFeatures[subscription.plan]?.includes(feature);
}

Usage-Based Billing

Report usage to Stripe for metered billing:

await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
  quantity: apiCallsThisMonth,
  timestamp: Math.floor(Date.now() / 1000),
  action: 'set',
});

Testing Checklist

Stripe CLI for local webhook testing:

stripe listen --forward-to localhost:3000/api/webhooks/stripe