@savvagent/nextjs
v1.0.1
Published
Next.js SDK for Savvagent feature flags with App Router support
Maintainers
Readme
@savvagent/nextjs
Next.js SDK for Savvagent with full App Router support, including Server Components, Client Components, Middleware, and Server Actions.
Installation
npm install @savvagent/nextjs
# or
pnpm add @savvagent/nextjs
# or
yarn add @savvagent/nextjsQuick Start
1. Initialize Server Client
// app/layout.tsx
import { initServerClient } from '@savvagent/nextjs/server';
initServerClient({
apiKey: process.env.SAVVAGENT_API_KEY!,
applicationId: process.env.SAVVAGENT_APP_ID,
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}2. Client Components
// app/components/feature.tsx
'use client';
import { useFlag } from '@savvagent/nextjs/client';
export function Feature() {
const { value, loading } = useFlag('new-feature');
if (loading) return <div>Loading...</div>;
return value ? <NewFeature /> : <OldFeature />;
}3. Server Components
// app/page.tsx
import { isEnabled } from '@savvagent/nextjs/server';
export default async function Page() {
const enabled = await isEnabled('new-layout');
return enabled ? <NewLayout /> : <OldLayout />;
}4. Middleware
// middleware.ts
import { initMiddlewareClient, createMiddleware } from '@savvagent/nextjs/middleware';
initMiddlewareClient({
apiKey: process.env.SAVVAGENT_API_KEY!,
});
export default createMiddleware({
async onRequest(request, client) {
const context = { user_id: request.cookies.get('user_id')?.value };
const maintenance = await client.isEnabled('maintenance-mode', context);
if (maintenance) {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}
},
});
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};API Reference
Server Components
initServerClient(config)
Initialize the server-side client. Call once in your root layout.
import { initServerClient } from '@savvagent/nextjs/server';
initServerClient({
apiKey: process.env.SAVVAGENT_API_KEY!,
applicationId: process.env.SAVVAGENT_APP_ID,
baseUrl: process.env.SAVVAGENT_BASE_URL,
});isEnabled(flagKey, context?)
Check if a flag is enabled in a Server Component.
import { isEnabled } from '@savvagent/nextjs/server';
export default async function Page() {
const enabled = await isEnabled('premium-features', {
user_id: 'user123',
attributes: { plan: 'pro' },
});
return <div>{enabled ? 'Premium' : 'Standard'}</div>;
}evaluate(flagKey, context?)
Get detailed flag evaluation result.
import { evaluate } from '@savvagent/nextjs/server';
export default async function Page() {
const result = await evaluate('beta-feature');
console.log({
value: result.value,
reason: result.reason, // 'cached' | 'evaluated' | 'default' | 'error'
metadata: result.metadata,
});
return <Component enabled={result.value} />;
}withFlag(flagKey, callback, context?)
Execute code conditionally based on flag value.
import { withFlag } from '@savvagent/nextjs/server';
export default async function Page() {
const data = await withFlag('use-new-api', async () => {
return await fetchFromNewAPI();
});
// data is null if flag is disabled
return data ? <NewView data={data} /> : <OldView />;
}createServerContext(overrides?)
Create a context object from Next.js request (automatically extracts cookies and headers).
import { createServerContext, isEnabled, getServerClient } from '@savvagent/nextjs/server';
export default async function Page() {
const context = await createServerContext({
attributes: { plan: 'enterprise' },
});
const client = getServerClient();
const enabled = await client.isEnabled('enterprise-features', context);
return <div>...</div>;
}Client Components
Use the 'use client' directive and import from @savvagent/nextjs/client:
'use client';
import {
SavvagentProvider,
useFlag,
useSavvagent,
useUser,
useTrackError,
} from '@savvagent/nextjs/client';All client-side APIs are the same as @savvagent/react. See the @savvagent/react documentation for details.
Example: Wrap Client-Side App
// app/providers.tsx
'use client';
import { SavvagentProvider } from '@savvagent/nextjs/client';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SavvagentProvider
config={{
apiKey: process.env.NEXT_PUBLIC_SAVVAGENT_API_KEY!,
enableRealtime: true,
}}
>
{children}
</SavvagentProvider>
);
}// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Route Handlers
evaluateForRequest(request, flagKey, context?)
Evaluate flags in API routes with request context.
// app/api/data/route.ts
import { evaluateForRequest } from '@savvagent/nextjs/server';
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const useNewAPI = await evaluateForRequest(request, 'new-api-version');
const data = useNewAPI
? await fetchFromNewAPI()
: await fetchFromOldAPI();
return Response.json(data);
}Server Actions
// app/actions.ts
'use server';
import { isEnabled, trackError } from '@savvagent/nextjs/server';
export async function submitForm(formData: FormData) {
const useNewValidation = await isEnabled('new-validation');
try {
if (useNewValidation) {
await validateWithNewLogic(formData);
} else {
await validateWithOldLogic(formData);
}
} catch (error) {
await trackError('new-validation', error as Error);
throw error;
}
}Middleware
initMiddlewareClient(config)
Initialize the middleware client.
import { initMiddlewareClient } from '@savvagent/nextjs/middleware';
initMiddlewareClient({
apiKey: process.env.SAVVAGENT_API_KEY!,
});createMiddleware(config)
Create a middleware function with custom logic.
import { createMiddleware } from '@savvagent/nextjs/middleware';
import { NextResponse } from 'next/server';
export default createMiddleware({
async onRequest(request, client) {
const context = {
user_id: request.cookies.get('user_id')?.value,
};
const maintenance = await client.isEnabled('maintenance-mode', context);
if (maintenance) {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}
},
});redirectIfEnabled(request, flagKey, redirectUrl, context?)
Redirect users when a flag is enabled.
import { redirectIfEnabled } from '@savvagent/nextjs/middleware';
export async function middleware(request: NextRequest) {
const redirect = await redirectIfEnabled(
request,
'force-upgrade',
'/upgrade'
);
if (redirect) return redirect;
}rewriteIfEnabled(request, flagKey, rewriteUrl, context?)
Rewrite requests when a flag is enabled (useful for A/B testing).
import { rewriteIfEnabled } from '@savvagent/nextjs/middleware';
export async function middleware(request: NextRequest) {
const rewrite = await rewriteIfEnabled(
request,
'beta-ui',
'/beta' + request.nextUrl.pathname
);
if (rewrite) return rewrite;
}Usage Patterns
SSR with User Context
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { isEnabled } from '@savvagent/nextjs/server';
export default async function DashboardPage() {
const cookieStore = await cookies();
const userId = cookieStore.get('user_id')?.value;
const showNewDashboard = await isEnabled('new-dashboard', {
user_id: userId,
});
return showNewDashboard ? <NewDashboard /> : <OldDashboard />;
}Mixed Server + Client
// app/page.tsx (Server Component)
import { isEnabled } from '@savvagent/nextjs/server';
import { ClientFeature } from './client-feature';
export default async function Page() {
const serverFlag = await isEnabled('server-feature');
return (
<div>
{serverFlag && <ServerComponent />}
<ClientFeature /> {/* Client component with its own flags */}
</div>
);
}// app/client-feature.tsx (Client Component)
'use client';
import { useFlag } from '@savvagent/nextjs/client';
export function ClientFeature() {
const { value } = useFlag('client-feature');
return value ? <NewUI /> : <OldUI />;
}Middleware-Based A/B Testing
// middleware.ts
import { initMiddlewareClient, getMiddlewareClient, getRequestContext } from '@savvagent/nextjs/middleware';
import { NextRequest, NextResponse } from 'next/server';
initMiddlewareClient({
apiKey: process.env.SAVVAGENT_API_KEY!,
});
export async function middleware(request: NextRequest) {
const client = getMiddlewareClient();
const context = getRequestContext(request);
// A/B test: 50% of users see variant B
const variantB = await client.isEnabled('checkout-variant-b', context);
if (variantB && request.nextUrl.pathname === '/checkout') {
return NextResponse.rewrite(new URL('/checkout-b', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/checkout'],
};Progressive Migration
// Gradually migrate to new implementation
import { isEnabled } from '@savvagent/nextjs/server';
export default async function Page() {
const useNewComponent = await isEnabled('use-new-component', {
attributes: {
rolloutPercentage: 10, // Start with 10% of users
},
});
return useNewComponent ? <NewComponent /> : <LegacyComponent />;
}Environment Variables
# Server-side API key (for Server Components, Route Handlers, Middleware)
SAVVAGENT_API_KEY=sdk_...
# Client-side API key (for Client Components with real-time updates)
NEXT_PUBLIC_SAVVAGENT_API_KEY=sdk_...
# Optional
SAVVAGENT_APP_ID=your-app-id
SAVVAGENT_BASE_URL=https://api.savvagent.comTypeScript Support
Full TypeScript support with type definitions for all APIs.
import type {
FlagClientConfig,
FlagContext,
FlagEvaluationResult,
} from '@savvagent/nextjs';Best Practices
- Use Server Components when possible - They're faster and reduce client bundle size
- Initialize server client in root layout - Ensures it's ready for all server components
- Use middleware for route-level flags - Great for redirects, rewrites, and A/B tests
- Separate client/server API keys - Use
NEXT_PUBLIC_prefix only for client-side keys - Leverage automatic context - Server helpers auto-extract user data from cookies/headers
- Track errors in new features - Use
trackError()to correlate errors with flag changes
License
MIT
