SaaS, MVP & Startup March 11, 2026 · 5 min read

How to Set Up Stripe Payments in Your Deployed SaaS App

How to Set Up Stripe Payments in Your Deployed SaaS App

Building a SaaS app with AI assistance? Sweet. But when it comes to actually collecting money from users, things get real fast. Stripe is the gold standard for payments, but integrating it properly in a production environment requires some DevOps know-how.

Let's walk through setting up Stripe payments the right way - from development to deployment.

Why Stripe for SaaS?

Stripe isn't just a payment processor - it's a complete billing platform built for SaaS. You get:

  • Recurring subscriptions out of the box
  • Automatic invoice generation
  • Tax calculation and compliance
  • Built-in dunning management
  • Webhooks for everything

For vibe coders building SaaS apps, Stripe handles the complex billing logic so you can focus on your product.

Environment Setup: Dev vs Production

First rule of Stripe: never mix test and live keys. Here's how to handle environments properly:

Environment Variables

# Development (.env.local)
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Production (set in your hosting platform)
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

Conditional API Initialization

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Always verify you're using the right environment
if (process.env.NODE_ENV === 'production' && 
    process.env.STRIPE_SECRET_KEY.includes('test')) {
  throw new Error('Using test keys in production!');
}

Basic Payment Flow Implementation

1. Create Payment Intent (Backend)

// /api/create-payment-intent
app.post('/api/create-payment-intent', async (req, res) => {
  try {
    const { amount, currency = 'usd' } = req.body;
    
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount * 100, // Stripe uses cents
      currency,
      metadata: {
        user_id: req.user.id,
        plan: req.body.plan
      }
    });
    
    res.json({ client_secret: paymentIntent.client_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

2. Handle Payment (Frontend)

// Using Stripe Elements
const handlePayment = async () => {
  const { error, paymentIntent } = await stripe.confirmCardPayment(
    clientSecret,
    {
      payment_method: {
        card: cardElement,
        billing_details: {
          email: user.email
        }
      }
    }
  );
  
  if (error) {
    setError(error.message);
  } else if (paymentIntent.status === 'succeeded') {
    // Payment succeeded - redirect or update UI
    window.location.href = '/dashboard';
  }
};

Subscription Management

For SaaS apps, subscriptions are where the magic happens:

Creating Products and Prices

// Create product (do this once, ideally via Stripe dashboard)
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'Advanced features for power users'
});

// Create recurring price
const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2999, // $29.99
  currency: 'usd',
  recurring: {
    interval: 'month'
  }
});

Subscription Creation

app.post('/api/create-subscription', async (req, res) => {
  try {
    const { customer_id, price_id } = req.body;
    
    const subscription = await stripe.subscriptions.create({
      customer: customer_id,
      items: [{ price: price_id }],
      payment_behavior: 'default_incomplete',
      payment_settings: { save_default_payment_method: 'on_subscription' },
      expand: ['latest_invoice.payment_intent']
    });
    
    res.json({
      subscription_id: subscription.id,
      client_secret: subscription.latest_invoice.payment_intent.client_secret
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Webhook Handling: The Critical Part

Webhooks are how Stripe tells your app about payment events. Getting this wrong means unhappy customers and lost revenue.

Webhook Endpoint Setup

app.post('/api/webhooks/stripe', 
  express.raw({ type: 'application/json' }), 
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;
    
    try {
      event = stripe.webhooks.constructEvent(
        req.body, 
        sig, 
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      console.log('Webhook signature verification failed.', err.message);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }
    
    // Handle the event
    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object);
        break;
      case 'invoice.payment_succeeded':
        await handleSubscriptionPayment(event.data.object);
        break;
      case 'customer.subscription.deleted':
        await handleSubscriptionCancellation(event.data.object);
        break;
      default:
        console.log(`Unhandled event type ${event.type}`);
    }
    
    res.json({ received: true });
  }
);

Deployment Considerations

SSL is Non-Negotiable

Stripe requires HTTPS in production. No exceptions. Most hosting platforms handle this automatically, but verify your SSL certificate is valid:

# Test your SSL setup
curl -I https://yourdomain.com/api/webhooks/stripe

Webhook URL Configuration

In your Stripe dashboard, set webhook endpoints for each environment:

  • Development: Use ngrok for local testing
  • Staging: https://staging.yourdomain.com/api/webhooks/stripe
  • Production: https://yourdomain.com/api/webhooks/stripe

Environment Variable Security

Never commit Stripe keys to version control. Use your hosting platform's environment variable system:

# For platforms like Vercel, Netlify, or Railway
vercel env add STRIPE_SECRET_KEY
railway variables set STRIPE_SECRET_KEY=sk_live_...

Testing in Production

Before going live:

  1. Test webhook delivery - Stripe dashboard shows delivery attempts
  2. Verify SSL certificates - Use Stripe's webhook testing tool
  3. Check error handling - Simulate failed payments
  4. Test subscription flows - Create, modify, cancel subscriptions

Monitoring Payment Health

// Add payment monitoring
const handlePaymentSuccess = async (paymentIntent) => {
  // Update user account
  await upgradeUserAccount(paymentIntent.metadata.user_id);
  
  // Log for monitoring
  console.log('Payment succeeded:', {
    amount: paymentIntent.amount,
    user_id: paymentIntent.metadata.user_id,
    timestamp: new Date().toISOString()
  });
  
  // Optional: Send to analytics
  analytics.track('Payment Completed', {
    revenue: paymentIntent.amount / 100,
    plan: paymentIntent.metadata.plan
  });
};

Common Gotchas

  1. Amount confusion - Stripe uses cents, not dollars
  2. Webhook timeouts - Respond quickly, process async
  3. Test mode mixing - Always check your environment
  4. Failed webhook retries - Stripe will retry failed webhooks
  5. Idempotency - Handle duplicate webhook events

Wrapping Up

Stripe payments aren't just about collecting money - they're about creating a smooth user experience that converts browsers into paying customers. With proper environment setup, webhook handling, and monitoring, you'll have a bulletproof payment system.

The key is treating payments as a core part of your infrastructure, not an afterthought. Get it right once, and focus on building features that make users want to pay you.

Remember: good payment flows feel invisible to users but require careful engineering behind the scenes. That's where proper deployment and hosting infrastructure makes all the difference.

Alex Hackney

Alex Hackney

DeployMyVibe

Ready to deploy?

Stop reading about it. Start shipping.

View Pricing