How to Add Subscriptions to a Next.js App
Next.js is the most popular framework for SaaS. Learn how to add authentication, subscription billing, and feature gating with StackBE.
In This Guide
Articles
Next.js + StackBE: The SaaS Stack
Next.js is the most popular React framework for building SaaS applications. Its server-side rendering, API routes, and middleware make it ideal for subscription-based products.
StackBE provides the billing backend: authentication, subscriptions, and entitlements. Together, they form a complete SaaS stack.
Architecture
Next.js App (frontend + API routes)
↕ SDK / API calls
StackBE (auth, billing, entitlements)
↕ Payment processing
Stripe (payment gateway via Connect)
What Lives Where
Next.js handles:
StackBE handles:
Getting Started
1. Install the SDK
npm install @stackbe/sdk
2. Configure Environment
# .env.local
STACKBE_API_KEY=sk_live_your_key
STACKBE_APP_ID=app_your_id
NEXT_PUBLIC_APP_URL=http://localhost:3000
3. Initialize the SDK
Create a shared StackBE client:
// lib/stackbe.tsimport { StackBE } from '@stackbe/sdk';
export const stackbe = new StackBE({
apiKey: process.env.STACKBE_API_KEY!,
appId: process.env.STACKBE_APP_ID!,
});Authentication
Magic Link Login Page
// app/login/page.tsx'use client';
import { useState } from 'react';
export default function LoginPage() {
const [email, setEmail] = useState('');const [sent, setSent] = useState(false);const handleSubmit = async (e: React.FormEvent) => {e.preventDefault();
await fetch('/api/auth/magic-link', {method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});setSent(true);
};if (sent) {return (<div>
<h1>Check your email</h1>
<p>We sent a login link to {email}</p>
</div>
);
}return (<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<button type="submit">Send Magic Link</button>
</form>
);
}Magic Link API Route
// app/api/auth/magic-link/route.tsimport { stackbe } from '@/lib/stackbe';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email } = await request.json();await stackbe.auth.sendMagicLink(email, {callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
});return NextResponse.json({ success: true });}Auth Callback Handler
// app/auth/callback/route.tsimport { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);const token = searchParams.get('token');if (!token) {return NextResponse.redirect('/login?error=missing_token');}// Set session cookiecookies().set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
});return NextResponse.redirect(new URL('/dashboard', request.url));}Auth Middleware
Protect routes with Next.js middleware:
// middleware.tsimport { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');if (!session) {return NextResponse.redirect(new URL('/login', request.url));}return NextResponse.next();}export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
};Subscription Management
Pricing Page with Checkout
// app/pricing/page.tsx'use client';
export default function PricingPage() {
const handleSubscribe = async (planId: string) => {const response = await fetch('/api/checkout', {method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId }),
});const { url } = await response.json();window.location.href = url;
};return (<div>
<div>
<h3>Free</h3>
<p>$0/month</p>
<ul>
<li>Basic features</li>
<li>100 API calls/month</li>
</ul>
<button onClick={() => handleSubscribe('plan_free')}>
Get Started
</button>
</div>
<div>
<h3>Pro</h3>
<p>$29/month</p>
<ul>
<li>All features</li>
<li>Unlimited API calls</li>
<li>Priority support</li>
</ul>
<button onClick={() => handleSubscribe('plan_pro')}>
Subscribe
</button>
</div>
</div>
);
}Checkout API Route
// app/api/checkout/route.tsimport { stackbe } from '@/lib/stackbe';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const session = cookies().get('session')?.value;const { planId } = await request.json();const { url } = await stackbe.checkout.createSession({customer: session, // or customer ID
planId,
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});return NextResponse.json({ url });}Entitlements & Feature Gating
Server Component with Entitlements
// app/dashboard/page.tsximport { stackbe } from '@/lib/stackbe';
import { cookies } from 'next/headers';
export default async function Dashboard() {
const session = cookies().get('session')?.value;// Get subscription with plan detailsconst subscription = await stackbe.subscriptions.get(session!, {expand: ['plan'],
});const { hasAccess: canExport } = await stackbe.entitlements.check(session!,
'advanced_export'
);
return (<div>
<h1>Dashboard</h1>
<p>Plan: {subscription?.plan?.name || 'Free'}</p>
{canExport ? (<ExportButton />
) : (
<UpgradePrompt feature="Advanced Export" />
)}
</div>
);
}API Route Protection
// app/api/export/route.tsimport { stackbe } from '@/lib/stackbe';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const session = cookies().get('session')?.value;const { hasAccess } = await stackbe.entitlements.check(session!,
'advanced_export'
);
if (!hasAccess) {return NextResponse.json({ error: 'Upgrade to Pro for this feature' },{ status: 403 });
}// Track usageawait stackbe.usage.track(session!, 'exports');// Perform export...return NextResponse.json({ downloadUrl: '...' });}Client-Side Entitlement Hook
// hooks/useEntitlements.ts'use client';
import { useEffect, useState } from 'react';
export function useEntitlements() {
const [entitlements, setEntitlements] = useState<string[]>([]);const [plan, setPlan] = useState<string>('free');const [loading, setLoading] = useState(true);useEffect(() => {
fetch('/api/me/entitlements')
.then(res => res.json())
.then(data => {
setEntitlements(data.entitlements);
setPlan(data.plan);
setLoading(false);
});}, []);const hasFeature = (feature: string) => entitlements.includes(feature);return { entitlements, plan, loading, hasFeature };}Handling Webhooks
Webhook Route
// app/api/webhooks/stackbe/route.tsimport { NextResponse } from 'next/server';
export async function POST(request: Request) {
const event = await request.json();switch (event.type) {
case 'subscription.created':
// New subscription - provision accessconsole.log('New subscription:', event.data);
break;
case 'subscription.canceled':
// Subscription canceled - handle cleanupconsole.log('Canceled:', event.data);
break;
case 'subscription.updated':
// Plan change - update entitlementsconsole.log('Updated:', event.data);
break;
case 'invoice.payment_failed':
// Payment failed - notify customerconsole.log('Payment failed:', event.data);
break;
}return NextResponse.json({ received: true });}Common Patterns
Subscription Status Banner
// components/SubscriptionBanner.tsx'use client';
import { useEntitlements } from '@/hooks/useEntitlements';
export function SubscriptionBanner() {
const { plan } = useEntitlements();if (plan === 'pro') return null;return (<div className="bg-yellow-900/20 border border-yellow-800 p-3 rounded">
<p>You're on the Free plan.</p>
<a href="/pricing" className="text-green-400 underline">
Upgrade to Pro
</a>
{' '}for unlimited access.</div>
);
}Usage Limit Display
// components/UsageDisplay.tsxexport async function UsageDisplay({ customerId }: { customerId: string }) {
const usage = await stackbe.usage.get(customerId, 'api_calls');return (<div>
<p>{usage.current} / {usage.limit} API calls this month</p>
<div className="w-full bg-neutral-800 rounded">
<div
className="bg-green-500 h-2 rounded"
style={{ width: `${(usage.current / usage.limit) * 100}%` }}
/>
</div>
</div>
);
}Deployment
Vercel (Recommended)
Next.js deploys naturally to Vercel:
1. Push to GitHub
2. Connect repo to Vercel
3. Set environment variables in Vercel dashboard
4. Deploy
Environment Variables
Set in Vercel:
Best Practices
Server-Side Entitlement Checks
Always verify entitlements server-side (API routes, server components). Client-side checks are for UI display only—they can be bypassed.
Cache Subscription Data
Don't fetch subscription status on every page load. Use React context or a shared fetch that caches the result per session.
Handle Loading States
Subscription checks are async. Show loading states while checking, not blank areas or flash of premium content.
Test with Multiple Plans
Create test customers on different plans. Verify feature gating works correctly for each tier.
Next Steps
1. Install the SDK: `npm install @stackbe/sdk`
2. Set up authentication: Magic link flow with callback
3. Define your plans: In StackBE dashboard
4. Implement checkout: Pricing page with Stripe redirect
5. Gate features: Server and client entitlement checks
6. Deploy: Vercel with environment variables
Related Resources
SaaS Billing Guide
Complete billing fundamentals
SaaS Auth Guide
Authentication best practices
Entitlements Guide
Feature gating patterns
StackBE vs Clerk
Compare auth approaches for Next.js
StackBE vs Supabase
Backend platform comparison
StackBE vs Firebase + Stripe
Purpose-built vs DIY
Ready to simplify your SaaS backend?
StackBE combines auth, billing, and entitlements in one API. Get started in minutes, not weeks.
Get Started FreeFrequently Asked Questions
Other Guides
The Complete Guide to SaaS Billing
Master SaaS billing from subscription models to payment recovery. Learn how to implement billing that scales with your business.
The Complete Guide to SaaS Authentication
Master SaaS authentication from passwordless to enterprise SSO. Learn how to implement secure, user-friendly auth for your application.
The Complete Guide to SaaS Entitlements
Master SaaS entitlements—the system that controls what features users can access based on their subscription. Learn why it's different from feature flags.
How to Add Subscriptions to a Chrome Extension
Chrome extensions have unique billing challenges. Learn how to implement subscriptions, handle authentication in a browser context, and gate features by plan.
How to Monetize Your API Product
Turning your API into a business requires more than just code. Learn pricing strategies, usage-based billing, and access control for API products.
How to Add Subscriptions to a Desktop Application
Desktop apps have unique subscription challenges: offline access, license validation, and cross-platform auth. Learn how to handle them with StackBE.