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-sdkpackage
Installation
pnpm add @supashiphq/react-sdkQuick 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.0Important: 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-sdkfor Client Components - Use
@supashiphq/js-sdkfor 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 FeaturesWithFallbacks3. 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 SupaProviderSolution: 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-keyMiddleware Not Working
Solution: Ensure middleware exports are correct and the matcher is properly configured:
export const config = {
matcher: ['/beta/:path*', '/admin/:path*'],
}