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/sdk

Environment 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:3000

Create 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'],
};

Next Steps