NextReady integrates with Stripe to provide a complete payment system for your SaaS application, supporting subscriptions, one-time payments, and usage-based billing.
NextReady uses Stripe for payment processing, providing a secure and reliable way to handle payments in your SaaS application. The payment system supports various features including subscriptions, one-time payments, and usage-based billing.
To use the payment system in NextReady, you need to set up a Stripe account and configure the necessary environment variables.
Set up the following environment variables in your .env.local
file:
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
STRIPE_CUSTOMER_PORTAL_URL=https://yourdomain.com/account/billing
Always use test keys during development. Only switch to live keys when you're ready to deploy to production. Never commit your Stripe API keys to version control.
Before using the payment system, you need to set up your products and prices in the Stripe Dashboard:
NextReady includes a complete subscription management system built on Stripe Subscriptions.
The subscription data is stored in your MongoDB database and linked to the user or organization:
// Example subscription data in User or Organization model
{
subscription: {
id: "sub_1234567890",
status: "active", // active, canceled, past_due, etc.
plan: "pro",
currentPeriodEnd: "2023-12-31T00:00:00.000Z",
cancelAtPeriodEnd: false,
stripeCustomerId: "cus_1234567890"
}
}
To create a new subscription, you redirect the user to the Stripe Checkout page:
// Client component
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function SubscribeButton({ plan, userId }) {
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleSubscribe = async () => {
try {
setLoading(true)
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plan,
userId,
}),
})
const data = await response.json()
if (data.url) {
router.push(data.url)
}
} catch (error) {
console.error('Error creating checkout session:', error)
} finally {
setLoading(false)
}
}
return (
<button
onClick={handleSubscribe}
disabled={loading}
className="btn btn-primary"
>
{loading ? 'Loading...' : `Subscribe to ${plan}`}
</button>
)
}
You can check the subscription status to determine user access to features:
// Helper function to check subscription
export function hasActiveSubscription(user) {
if (!user?.subscription) {
return false
}
const { status, currentPeriodEnd } = user.subscription
// Check if subscription is active
if (status !== 'active' && status !== 'trialing') {
return false
}
// Check if subscription is expired
if (new Date(currentPeriodEnd) < new Date()) {
return false
}
return true
}
// Check if user has access to a specific feature
export function hasFeatureAccess(user, featureName) {
if (!hasActiveSubscription(user)) {
return false
}
const planFeatures = getPlanFeatures(user.subscription.plan)
return planFeatures.includes(featureName)
}
NextReady uses Stripe Checkout to handle the payment process. This provides a secure, pre-built payment page that you can customize.
The checkout process starts by creating a Stripe Checkout session:
// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
})
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { plan } = await req.json()
// Get price ID based on plan
const priceId = getPriceIdForPlan(plan)
if (!priceId) {
return NextResponse.json(
{ error: 'Invalid plan selected' },
{ status: 400 }
)
}
// Create checkout session
const checkoutSession = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing?canceled=true`,
customer_email: session.user.email,
metadata: {
userId: session.user.id,
plan,
},
})
return NextResponse.json({ url: checkoutSession.url })
} catch (error) {
console.error('Error creating checkout session:', error)
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
)
}
}
// Helper function to get price ID for a plan
function getPriceIdForPlan(plan: string): string | null {
const prices = {
basic: process.env.STRIPE_PRICE_BASIC,
pro: process.env.STRIPE_PRICE_PRO,
enterprise: process.env.STRIPE_PRICE_ENTERPRISE,
}
return prices[plan] || null
}
After a successful checkout, Stripe will redirect the user to your success URL. You can display a confirmation message and update the UI:
// Client component for success page
'use client'
import { useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
export default function BillingPage() {
const searchParams = useSearchParams()
const [status, setStatus] = useState<'success' | 'canceled' | null>(null)
useEffect(() => {
if (searchParams.get('success')) {
setStatus('success')
} else if (searchParams.get('canceled')) {
setStatus('canceled')
}
}, [searchParams])
return (
<div>
{status === 'success' && (
<div className="p-4 bg-green-50 text-green-700 rounded-lg mb-4">
Your subscription has been successfully activated! You now have access to all features.
</div>
)}
{status === 'canceled' && (
<div className="p-4 bg-yellow-50 text-yellow-700 rounded-lg mb-4">
Your checkout was canceled. No charges were made.
</div>
)}
{/* Rest of billing page content */}
</div>
)
}
Webhooks are essential for handling asynchronous events from Stripe, such as successful payments, subscription updates, and failed payments.
https://yourdomain.com/api/webhooks/stripe
)checkout.session.completed
, invoice.paid
, etc.)NextReady includes a webhook handler to process Stripe events:
// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { buffer } from 'micro'
import dbConnect from '@/lib/mongodb'
import User from '@/models/User'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
})
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
export async function POST(req: NextRequest) {
try {
const buf = await buffer(req)
const sig = req.headers.get('stripe-signature')
if (!sig) {
return NextResponse.json(
{ error: 'Missing stripe-signature header' },
{ status: 400 }
)
}
// Verify webhook signature
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
buf,
sig,
webhookSecret
)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return NextResponse.json(
{ error: 'Webhook signature verification failed' },
{ status: 400 }
)
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Update user subscription
await handleCheckoutSessionCompleted(session)
break
}
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice
// Update subscription period
await handleInvoicePaid(invoice)
break
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
// Update subscription status
await handleSubscriptionUpdated(subscription)
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
// Cancel subscription
await handleSubscriptionDeleted(subscription)
break
}
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
} catch (error) {
console.error('Webhook error:', error)
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
)
}
}
// Helper functions for handling different events
async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
if (!session.metadata?.userId || !session.customer) {
return
}
await dbConnect()
const user = await User.findById(session.metadata.userId)
if (!user) {
return
}
// Get subscription details from Stripe
const subscriptions = await stripe.subscriptions.list({
customer: session.customer as string,
limit: 1,
})
if (subscriptions.data.length === 0) {
return
}
const subscription = subscriptions.data[0]
// Update user subscription
user.subscription = {
id: subscription.id,
status: subscription.status,
plan: session.metadata.plan,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
stripeCustomerId: session.customer,
}
await user.save()
}
Stripe Customer Portal allows users to manage their subscriptions, update payment methods, and view invoices.
// src/app/api/customer-portal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import Stripe from 'stripe'
import User from '@/models/User'
import dbConnect from '@/lib/mongodb'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16',
})
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
await dbConnect()
const user = await User.findById(session.user.id)
if (!user?.subscription?.stripeCustomerId) {
return NextResponse.json(
{ error: 'No active subscription found' },
{ status: 400 }
)
}
// Create customer portal session
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.subscription.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account/billing`,
})
return NextResponse.json({ url: portalSession.url })
} catch (error) {
console.error('Error creating customer portal session:', error)
return NextResponse.json(
{ error: 'Failed to create customer portal session' },
{ status: 500 }
)
}
}
// Client component
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function ManageSubscriptionButton() {
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleManageSubscription = async () => {
try {
setLoading(true)
const response = await fetch('/api/customer-portal', {
method: 'POST',
})
const data = await response.json()
if (data.url) {
router.push(data.url)
}
} catch (error) {
console.error('Error opening customer portal:', error)
} finally {
setLoading(false)
}
}
return (
<button
onClick={handleManageSubscription}
disabled={loading}
className="btn btn-outline"
>
{loading ? 'Loading...' : 'Manage Subscription'}
</button>
)
}
NextReady provides a set of API endpoints for managing payments and subscriptions.
Endpoint | Method | Description |
---|---|---|
/api/checkout | POST | Create a Stripe Checkout session for subscription or one-time payment |
/api/customer-portal | POST | Create a Stripe Customer Portal session for managing subscriptions |
/api/webhooks/stripe | POST | Handle Stripe webhook events |
/api/subscriptions | GET | Get current user's subscription details |
/api/subscriptions/cancel | POST | Cancel current subscription at period end |
// src/app/api/subscriptions/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth-config'
import dbConnect from '@/lib/mongodb'
import User from '@/models/User'
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
await dbConnect()
const user = await User.findById(session.user.id)
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Return subscription details
return NextResponse.json({
subscription: user.subscription || null,
})
} catch (error) {
console.error('Error fetching subscription:', error)
return NextResponse.json(
{ error: 'Failed to fetch subscription details' },
{ status: 500 }
)
}
}
Now that you understand how payments work in NextReady, you can: