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:
- Test webhook delivery - Stripe dashboard shows delivery attempts
- Verify SSL certificates - Use Stripe's webhook testing tool
- Check error handling - Simulate failed payments
- 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
- Amount confusion - Stripe uses cents, not dollars
- Webhook timeouts - Respond quickly, process async
- Test mode mixing - Always check your environment
- Failed webhook retries - Stripe will retry failed webhooks
- 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
DeployMyVibe