Stripe Subscriptions
Implement subscription payments with Stripe
Stripe Subscriptions
Set up subscription-based payments using Stripe.
Prerequisites
- A Stripe account
- Stripe API keys (found in your Stripe dashboard)
Setup
1. Install Stripe
pnpm add stripe @stripe/stripe-js2. Configure Environment Variables
Add to your .env.local:
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...3. Create Stripe Client
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});Creating Products
Create products in your Stripe dashboard or via API:
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Access to all premium features',
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2000, // $20.00
currency: 'usd',
recurring: {
interval: 'month',
},
});Checkout Flow
Create Checkout Session
// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const session = await auth();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const { priceId } = await request.json();
try {
const checkoutSession = await stripe.checkout.sessions.create({
customer_email: session.user.email,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing?canceled=true`,
metadata: {
userId: session.user.id,
},
});
return NextResponse.json({ sessionId: checkoutSession.id });
} catch (error) {
console.error('Stripe error:', error);
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}Client-Side Checkout Button
'use client';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Button } from '@/components/ui/button';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
export function CheckoutButton({ priceId }: { priceId: string }) {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { sessionId } = await response.json();
const stripe = await stripePromise;
if (stripe) {
await stripe.redirectToCheckout({ sessionId });
}
} catch (error) {
console.error('Checkout error:', error);
} finally {
setLoading(false);
}
};
return (
<Button onClick={handleCheckout} disabled={loading}>
{loading ? 'Loading...' : 'Subscribe Now'}
</Button>
);
}Webhook Handling
Handle Stripe events with webhooks:
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { db } from '@/prisma';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error) {
console.error('Webhook signature verification failed:', error);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// Update user subscription in database
await db.user.update({
where: { id: session.metadata?.userId },
data: {
stripeCustomerId: session.customer as string,
subscriptionStatus: 'active',
},
});
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await db.user.update({
where: { stripeCustomerId: subscription.customer as string },
data: {
subscriptionStatus: subscription.status,
},
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await db.user.update({
where: { stripeCustomerId: subscription.customer as string },
data: {
subscriptionStatus: 'canceled',
},
});
break;
}
}
return NextResponse.json({ received: true });
}Customer Portal
Allow users to manage their subscriptions:
// app/api/portal/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
import { db } from '@/prisma';
import { NextResponse } from 'next/server';
export async function POST() {
const session = await auth();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const user = await db.user.findUnique({
where: { id: session.user.id },
});
if (!user?.stripeCustomerId) {
return NextResponse.json(
{ error: 'No subscription found' },
{ status: 404 }
);
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
});
return NextResponse.json({ url: portalSession.url });
}Check Subscription Status
// lib/subscription.ts
import { db } from '@/prisma';
export async function hasActiveSubscription(userId: string) {
const user = await db.user.findUnique({
where: { id: userId },
});
return user?.subscriptionStatus === 'active';
}Testing
Use Stripe test mode with test card numbers:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002
Next Steps
- Learn about Customer Support
- Explore Analytics