Next.js Integration
Complete guide to integrating StackBE with Next.js App Router.
What You'll Build
By the end of this guide, you'll have:
- Magic link authentication for your users
- Subscription checkout with Stripe
- Protected routes based on subscription status
- Feature gating based on plan entitlements
1. Installation & Setup
bash
npm install @stackbe/sdkEnvironment Variables
bash
# .env.local
STACKBE_API_KEY=sbk_live_your_api_key
STACKBE_APP_ID=your_app_id
STACKBE_WEBHOOK_SECRET=whsec_your_webhook_secret
NEXT_PUBLIC_APP_URL=http://localhost:3000Create SDK Instance
typescript
// lib/stackbe.ts
import { StackBE } from '@stackbe/sdk';
export const stackbe = new StackBE({
apiKey: process.env.STACKBE_API_KEY!,
appId: process.env.STACKBE_APP_ID!,
});2. Magic Link Authentication
Login Page
typescript
// 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 className="text-center">
<h1>Check your email</h1>
<p>We sent a magic 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
typescript
// app/api/auth/magic-link/route.ts
import { stackbe } from '@/lib/stackbe';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email } = await request.json();
const callbackUrl = process.env.NODE_ENV === 'production'
? `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`
: 'http://localhost:3000/auth/callback';
await stackbe.auth.sendMagicLink(email, {
redirectUrl: callbackUrl,
});
return NextResponse.json({ success: true });
}Auth Callback
typescript
// app/auth/callback/route.ts
import { stackbe } from '@/lib/stackbe';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const token = searchParams.get('token');
if (!token) {
redirect('/login?error=missing_token');
}
try {
const result = await stackbe.auth.verifyToken(token);
// Set session cookie
cookies().set('session', result.sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
redirect('/dashboard');
} catch (error) {
redirect('/login?error=invalid_token');
}
}3. Session Management
Get Current User
typescript
// lib/auth.ts
import { cookies } from 'next/headers';
import { stackbe } from './stackbe';
export async function getCurrentUser() {
const sessionToken = cookies().get('session')?.value;
if (!sessionToken) {
return null;
}
try {
const session = await stackbe.auth.getSession(sessionToken);
return session;
} catch {
return null;
}
}
export async function requireAuth() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
return user;
}Protected Page
typescript
// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth';
export default async function DashboardPage() {
const user = await requireAuth();
return (
<div>
<h1>Welcome, {user.email}</h1>
<p>Plan: {user.entitlements?.planName || 'Free'}</p>
</div>
);
}4. Subscription Checkout
Pricing Page
typescript
// app/pricing/page.tsx
import { getCurrentUser } from '@/lib/auth';
const plans = [
{ id: 'plan_free', name: 'Free', price: 0 },
{ id: 'plan_pro', name: 'Pro', price: 29 },
{ id: 'plan_team', name: 'Team', price: 99 },
];
export default async function PricingPage() {
const user = await getCurrentUser();
return (
<div className="grid grid-cols-3 gap-8">
{plans.map((plan) => (
<div key={plan.id} className="p-6 border rounded-lg">
<h2>{plan.name}</h2>
<p>${plan.price}/month</p>
<form action="/api/checkout" method="POST">
<input type="hidden" name="planId" value={plan.id} />
<button type="submit">
{user ? 'Subscribe' : 'Get Started'}
</button>
</form>
</div>
))}
</div>
);
}Checkout API Route
typescript
// app/api/checkout/route.ts
import { stackbe } from '@/lib/stackbe';
import { getCurrentUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
export async function POST(request: Request) {
const formData = await request.formData();
const planId = formData.get('planId') as string;
const user = await getCurrentUser();
const { url } = await stackbe.checkout.createSession({
customerId: user?.customerId,
email: user?.email, // For new users
planId,
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
redirect(url);
}5. Webhook Handler
typescript
// app/api/webhooks/stackbe/route.ts
import { stackbe } from '@/lib/stackbe';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('x-stackbe-signature')!;
try {
const event = stackbe.webhooks.verify(body, signature, {
secret: process.env.STACKBE_WEBHOOK_SECRET!,
});
switch (event.type) {
case 'subscription.created':
// New subscription - maybe send welcome email
console.log('New subscription:', event.data.subscription);
break;
case 'subscription.canceled':
// Subscription ended - update your database
console.log('Canceled:', event.data.subscription);
break;
case 'payment.failed':
// Payment failed - notify the user
console.log('Payment failed:', event.data);
break;
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
}6. Feature Gating
Server Component
typescript
// app/dashboard/export/page.tsx
import { requireAuth } from '@/lib/auth';
import { stackbe } from '@/lib/stackbe';
import { redirect } from 'next/navigation';
export default async function ExportPage() {
const user = await requireAuth();
// Check if user has export feature
const { hasAccess } = await stackbe.entitlements.check(
user.customerId,
'xlsx_export'
);
if (!hasAccess) {
redirect('/pricing?feature=xlsx_export');
}
return (
<div>
<h1>Export Data</h1>
{/* Export functionality */}
</div>
);
}Feature Gate Component
typescript
// components/FeatureGate.tsx
import { stackbe } from '@/lib/stackbe';
import { getCurrentUser } from '@/lib/auth';
interface FeatureGateProps {
feature: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export async function FeatureGate({
feature,
children,
fallback,
}: FeatureGateProps) {
const user = await getCurrentUser();
if (!user) {
return fallback || null;
}
const { hasAccess } = await stackbe.entitlements.check(
user.customerId,
feature
);
if (!hasAccess) {
return fallback || <UpgradePrompt feature={feature} />;
}
return <>{children}</>;
}
// Usage:
// <FeatureGate feature="advanced_analytics">
// <AnalyticsDashboard />
// </FeatureGate>7. Middleware (Optional)
Use Next.js middleware to protect routes at the edge.
typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Redirect logged-in users away from login
if (request.nextUrl.pathname === '/login') {
if (session) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};