Skip to Content

Next.js

Complete guide for integrating Supaship feature flags with Next.js, including App Router, Pages Router, Server Components, and Edge Runtime support.

Requirements

  • Next.js 12.0+
  • React 16.8+ (for hooks support)
  • Use @supashiphq/react-sdk package

Installation

pnpm add @supashiphq/react-sdk

Quick Start

Next.js App Router (Next.js 13+)

// app/providers.tsx 'use client' import { SupaProvider, FeaturesWithFallbacks, InferFeatures, } from '@supashiphq/react-sdk' const FEATURE_FLAGS = { 'new-hero': false, 'theme-config': { mode: 'light' as const, showLogo: true }, 'beta-features': [] as string[], } satisfies FeaturesWithFallbacks // REQUIRED: for type safety declare module '@supashiphq/react-sdk' { interface Features extends InferFeatures<typeof FEATURE_FLAGS> {} } export function Providers({ children }: { children: React.ReactNode }) { return ( <SupaProvider config={{ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, features: FEATURE_FLAGS, context: { version: process.env.NEXT_PUBLIC_APP_VERSION, }, }}> {children} </SupaProvider> ) } // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang='en'> <body> <Providers>{children}</Providers> </body> </html> ) } // app/page.tsx 'use client' import { useFeature } from '@supashiphq/react-sdk' export default function HomePage() { const { feature: newHero, isLoading } = useFeature('new-hero') if (isLoading) return <div>Loading...</div> return <main>{newHero ? <NewHeroSection /> : <OldHeroSection />}</main> }

Next.js Pages Router (Next.js 12 and below)

// pages/_app.tsx import { SupaProvider, FeaturesWithFallbacks, InferFeatures, } from '@supashiphq/react-sdk' import type { AppProps } from 'next/app' const FEATURE_FLAGS = { 'new-homepage': false, 'theme-config': { mode: 'light' as const }, } satisfies FeaturesWithFallbacks declare module '@supashiphq/react-sdk' { interface Features extends InferFeatures<typeof FEATURE_FLAGS> {} } export default function App({ Component, pageProps }: AppProps) { return ( <SupaProvider config={{ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, features: FEATURE_FLAGS, }}> <Component {...pageProps} /> </SupaProvider> ) }

Environment Variables

Create a .env.local file:

NEXT_PUBLIC_SUPASHIP_API_KEY=your-api-key-here NEXT_PUBLIC_APP_VERSION=1.0.0

Important: In Next.js, environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Never expose sensitive keys without the prefix.

App Router Patterns

Server Components with Feature Flags

For Server Components, you can use the JavaScript SDK directly:

// app/page.tsx (Server Component) import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export default async function HomePage() { // Fetch features on the server const features = await client.getFeatures(['new-hero', 'show-banner']) return ( <main> {features['new-hero'] ? <NewHeroSection /> : <OldHeroSection />} {features['show-banner'] && <Banner />} </main> ) }

Client Components with React Hooks

For interactive components, use the React SDK:

// app/components/Dashboard.tsx 'use client' import { useFeatures } from '@supashiphq/react-sdk' export function Dashboard() { const { features, isLoading } = useFeatures([ 'new-dashboard', 'beta-mode', 'show-sidebar', ]) if (isLoading) return <LoadingSpinner /> return ( <div className={features['new-dashboard'] ? 'new-layout' : 'old-layout'}> {features['show-sidebar'] && <Sidebar />} {features['beta-mode'] && <BetaBadge />} <MainContent /> </div> ) }

Middleware with Feature Flags

Use middleware to conditionally render pages or redirect based on feature flags:

// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export async function middleware(request: NextRequest) { // Get user context from cookies or headers const userId = request.cookies.get('userId')?.value const email = request.cookies.get('email')?.value // Check feature flag const feature = await client.getFeature('beta-access', { context: { userId, email, path: request.nextUrl.pathname, }, }) // Redirect if feature is disabled if (request.nextUrl.pathname.startsWith('/beta') && !feature) { return NextResponse.redirect(new URL('/404', request.url)) } return NextResponse.next() } export const config = { matcher: '/beta/:path*', }

Route Handlers (API Routes)

Use feature flags in API routes:

// app/api/features/route.ts import { NextResponse } from 'next/server' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export async function GET(request: Request) { const { searchParams } = new URL(request.url) const userId = searchParams.get('userId') const features = await client.getFeatures(['feature-a', 'feature-b'], { context: { userId }, }) return NextResponse.json(features) }

Server Actions

Use feature flags in Server Actions:

// app/actions.ts 'use server' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export async function processOrder(orderId: string, userId: string) { const feature = await client.getFeature('new-checkout-flow', { context: { userId }, }) if (feature) { // Use new checkout flow return await processOrderV2(orderId) } // Use old checkout flow return await processOrderV1(orderId) }

Edge Runtime

Use feature flags in Edge Runtime (Edge API Routes, Edge Middleware):

// app/api/edge/route.ts import { createClient } from '@supashiphq/js-sdk' export const runtime = 'edge' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export async function GET(request: Request) { const feature = await client.getFeature('edge-feature') return new Response(JSON.stringify({ enabled: feature }), { headers: { 'Content-Type': 'application/json' }, }) }

Pages Router Patterns

Custom App with User Context

// pages/_app.tsx import { useState, useEffect } from 'react' import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk' import type { AppProps } from 'next/app' import { useAuth } from '../hooks/useAuth' const FEATURE_FLAGS = { 'new-dashboard': false, } satisfies FeaturesWithFallbacks export default function App({ Component, pageProps }: AppProps) { const { user } = useAuth() return ( <SupaProvider config={{ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, features: FEATURE_FLAGS, context: { userId: user?.id, email: user?.email, plan: user?.plan, }, }}> <Component {...pageProps} /> </SupaProvider> ) }

API Routes (Pages Router)

// pages/api/features.ts import type { NextApiRequest, NextApiResponse } from 'next' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { const { userId } = req.query const features = await client.getFeatures(['feature-a', 'feature-b'], { context: { userId }, }) res.status(200).json(features) }

getServerSideProps

// pages/dashboard.tsx import type { GetServerSideProps } from 'next' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export const getServerSideProps: GetServerSideProps = async context => { const userId = context.req.cookies.userId const features = await client.getFeatures(['new-dashboard', 'beta-mode'], { context: { userId }, }) return { props: { features, }, } } export default function Dashboard({ features }: { features: any }) { return ( <div> {features['new-dashboard'] ? <NewDashboard /> : <OldDashboard />} {features['beta-mode'] && <BetaBadge />} </div> ) }

getStaticProps (with ISR)

// pages/products.tsx import type { GetStaticProps } from 'next' import { createClient } from '@supashiphq/js-sdk' const client = createClient({ apiKey: process.env.NEXT_PUBLIC_SUPASHIP_API_KEY!, environment: process.env.NODE_ENV!, }) export const getStaticProps: GetStaticProps = async () => { // Fetch features at build time (or revalidate with ISR) const features = await client.getFeatures(['new-product-page']) return { props: { features, }, revalidate: 60, // Revalidate every 60 seconds } } export default function Products({ features }: { features: any }) { return ( <div> {features['new-product-page'] ? ( <NewProductPage /> ) : ( <OldProductPage /> )} </div> ) }

Advanced Patterns

Dynamic Context Updates

Update context when user state changes:

// app/components/UserProvider.tsx 'use client' import { useEffect } from 'react' import { useFeatureContext } from '@supashiphq/react-sdk' import { useAuth } from '../hooks/useAuth' export function UserProvider({ children }: { children: React.ReactNode }) { const { updateContext } = useFeatureContext() const { user } = useAuth() useEffect(() => { if (user) { updateContext({ userId: user.id, email: user.email, plan: user.plan, segment: user.segment, }) } }, [user, updateContext]) return <>{children}</> }

Streaming with Suspense

Use React Suspense for better loading states:

// app/components/FeatureContent.tsx 'use client' import { Suspense } from 'react' import { useFeature } from '@supashiphq/react-sdk' function FeatureContent() { const { feature } = useFeature('new-content') return <div>{feature ? <NewContent /> : <OldContent />}</div> } export function FeatureContentWithSuspense() { return ( <Suspense fallback={<LoadingSkeleton />}> <FeatureContent /> </Suspense> ) }

Parallel Data Fetching

Fetch features in parallel with other data:

// app/page.tsx export default async function HomePage() { // Fetch in parallel const [products, features] = await Promise.all([ fetchProducts(), client.getFeatures(['new-layout', 'show-banner']), ]) return ( <main> {features['show-banner'] && <Banner />} <ProductList products={products} layout={features['new-layout']} /> </main> ) }

Feature Flag Guards

Create reusable route guards:

// lib/feature-guard.tsx 'use client' import { useFeature } from '@supashiphq/react-sdk' import { useRouter } from 'next/navigation' import { useEffect } from 'react' export function FeatureGuard({ feature, redirectTo = '/404', children, }: { feature: string redirectTo?: string children: React.ReactNode }) { const { feature: isEnabled, isLoading } = useFeature(feature) const router = useRouter() useEffect(() => { if (!isLoading && !isEnabled) { router.push(redirectTo) } }, [isEnabled, isLoading, router, redirectTo]) if (isLoading) return <LoadingSpinner /> if (!isEnabled) return null return <>{children}</> } // Usage export default function BetaPage() { return ( <FeatureGuard feature='beta-access'> <BetaContent /> </FeatureGuard> ) }

Testing

Mock Feature Flags in Tests

// __tests__/setup.tsx import { SupaProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk' export function TestProviders({ children, features = {}, }: { children: React.ReactNode features?: FeaturesWithFallbacks }) { return ( <SupaProvider config={{ apiKey: 'test-key', environment: 'test', features: features satisfies FeaturesWithFallbacks, }}> {children} </SupaProvider> ) } // __tests__/page.test.tsx import { render, screen } from '@testing-library/react' import { TestProviders } from './setup' import HomePage from '../app/page' describe('HomePage', () => { it('shows new hero when enabled', () => { render( <TestProviders features={{ 'new-hero': true }}> <HomePage /> </TestProviders>, ) expect(screen.getByText('New Hero')).toBeInTheDocument() }) })

Best Practices

1. Separate Client and Server Usage

  • Use @supashiphq/react-sdk for Client Components
  • Use @supashiphq/js-sdk for Server Components, API routes, and middleware

2. Environment-Specific Configuration

// lib/features.ts import { FeaturesWithFallbacks } from '@supashiphq/react-sdk' export const FEATURE_FLAGS = { 'new-dashboard': false, 'beta-mode': process.env.NODE_ENV === 'development', } satisfies FeaturesWithFallbacks

3. Cache Feature Flags

For Server Components, consider caching:

import { unstable_cache } from 'next/cache' const getCachedFeatures = unstable_cache( async (userId: string) => { return await client.getFeatures(['feature-a', 'feature-b'], { context: { userId }, }) }, ['features'], { revalidate: 60 }, // Cache for 60 seconds )

4. Type Safety

Always use type augmentation:

// lib/features.ts import { FeaturesWithFallbacks, InferFeatures } from '@supashiphq/react-sdk' export const FEATURE_FLAGS = { 'new-feature': false, } satisfies FeaturesWithFallbacks declare module '@supashiphq/react-sdk' { interface Features extends InferFeatures<typeof FEATURE_FLAGS> {} }

Troubleshooting

Client Component Error

Error: useFeature must be used within a SupaProvider

Solution: Ensure you’re using 'use client' directive and the component is within the provider:

'use client' // ✅ Add this directive import { useFeature } from '@supashiphq/react-sdk'

Environment Variables Not Working

Solution: Ensure variables are prefixed with NEXT_PUBLIC_ and restart the dev server:

# .env.local NEXT_PUBLIC_SUPASHIP_API_KEY=your-key

Middleware Not Working

Solution: Ensure middleware exports are correct and the matcher is properly configured:

export const config = { matcher: ['/beta/:path*', '/admin/:path*'], }
Last updated on