Basestack Docs
Backend

Next.js (Official SDK)

Seamless integration with Next.js streamlines the testing and development of features across production and various environments. This SDK effortlessly integrates into any Next.js-based environment, allowing you to leverage this capability in projects like Next.js App Router or Pages Router.

The Basestack Next.js SDK provides optimized server-side rendering (SSR) support, enabling you to fetch feature flags on the server and hydrate them on the client. This approach eliminates loading states, improves SEO, and ensures consistent flag values between server and client renders.

Getting started

We only support ESM (ECMAScript Modules) in the browser or server environment. If you are using CommonJS, you will need to use a bundler like Webpack or Rollup to bundle your code.

Examples

Install

First, let's install some packages! The Next.js SDK requires both @basestack/flags-react for React-specific functionality and @basestack/flags-js as the core SDK dependency.

Install the SDK
npm install @basestack/flags-react @basestack/flags-js
pnpm install @basestack/flags-react @basestack/flags-js
yarn add @basestack/flags-react @basestack/flags-js
bun add @basestack/flags-react @basestack/flags-js

Environment Variables

Configure your environment variables to connect to your Basestack project. The NEXT_PUBLIC_ prefix makes these variables available in both server and client components.

Make sure to add your .env file to .gitignore to keep your keys secure. Never commit sensitive credentials to version control.

.env
# BASESTACK FEATURE FLAGS
NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL="https://flags.basestack.co/api/v1" # or your own URL
NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY=""
NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY=""

You can find your project and environment keys in your Basestack Feature Flags Dashboard.

App Router Implementation

The App Router is Next.js's modern routing system that provides better performance and developer experience. This implementation leverages server components to fetch flags on the server, reducing client-side loading states and improving initial page load performance.

Setup the Provider

First, create a client-side provider component that wraps your application. This provider manages the feature flags context and handles client-side flag fetching when needed.

The Providers component accepts initialFlags from the server, which are used to hydrate the client-side state. The preload prop controls whether the SDK should fetch flags on the client if no initial flags are provided.

src/libs/feature-flags/providers.tsx
"use client";

import type { ReactNode } from "react";
import type { Flag } from "@basestack/flags-js";
import { FlagsProvider } from "@basestack/flags-react/client";
import type { SDKConfig } from "@basestack/flags-js";

export const flagsConfig: SDKConfig = {
  baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
  projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
  environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
};


export interface ProvidersProps {
  readonly children: ReactNode;
  readonly initialFlags?: Flag[];
}

export function Providers({ children, initialFlags }: ProvidersProps) {
  return (
    <FlagsProvider
      config={flagsConfig}
      initialFlags={initialFlags}
      preload={!initialFlags?.length}
    >
      {children}
    </FlagsProvider>
  );
}

Root Layout Setup

In your root layout, fetch flags on the server using fetchFlags and pass them to the Providers component. The FlagsHydrationScript ensures flags are available immediately on the client, preventing hydration mismatches.

This approach provides the best performance because:

  • Flags are fetched during server-side rendering
  • No client-side loading states for initial render
  • Flags are immediately available to all client components
  • SEO-friendly content with flags already evaluated
app/layout.tsx
import type { ReactNode } from "react";
import {
  FlagsHydrationScript,
  fetchFlags,
} from "@basestack/flags-react/server";
import { Providers } from "@/libs/feature-flags/providers";

export const metadata = {
  title: "Basestack Flags • Next.js App Router",
};

export default async function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  const flags = await fetchFlags({
    baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
    projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
    environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
  });

  return (
    <html lang="en">
      <body>
        <Providers initialFlags={flags}>{children}</Providers>
        <FlagsHydrationScript flags={flags} />
      </body>
    </html>
  );
}

Using Flags in Client Components

Use the useFlag hook in any client component to access feature flags. The hook returns the flag's enabled state, payload, and loading status. TypeScript generics allow you to type the payload for better type safety.

app/page.tsx
"use client";

import { useFlag } from "@basestack/flags-react/client";

export default function HomePage() {
  const { enabled, payload, isLoading } = useFlag<{ variant?: string }>(
    "header"
  );

  if (isLoading)
    return <p style={{ fontFamily: "sans-serif" }}>Checking feature flags…</p>;

  return (
    <main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
      <h1>Next.js App Router Example</h1>
      <p>
        Flag <code>header</code> is {enabled ? "enabled" : "disabled"}
      </p>
      {enabled && payload ? (
        <p>Variant: {JSON.stringify(payload, null, 2)}</p>
      ) : null}
    </main>
  );
}

Server Actions

Server Actions allow you to fetch flags on-demand from client components. This is useful for refreshing flag values or fetching flags conditionally based on user interactions.

Create a server action that uses the server SDK to fetch flags:

app/server-actions/actions.ts
"use server";

import type { Flag } from "@basestack/flags-js";
import { fetchFlag } from "@basestack/flags-react/server";

export async function getHeaderFlagAction(slug = "header"): Promise<Flag> {
  return fetchFlag(slug, {
    baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
    projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
    environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
  });
}

Then use the server action in a client component with React's useTransition hook for optimal UX:

app/server-actions/demo.tsx
"use client";

import type { Flag } from "@basestack/flags-js";
import { useState, useTransition } from "react";
import { getHeaderFlagAction } from "./actions";

export function ServerActionDemo() {
  const [flag, setFlag] = useState<Flag | null>(null);
  const [isPending, startTransition] = useTransition();

  const handleRefresh = () => {
    startTransition(async () => {
      const latest = await getHeaderFlagAction();
      setFlag(latest);
    });
  };

  return (
    <section style={{ fontFamily: "sans-serif", padding: "1rem", border: "1px solid #e5e5e5" }}>
      <h2>Server Function Demo</h2>
      <p>
        This button calls a Next.js Server Action that uses the Basestack server SDK to fetch the
        <code>header</code> flag before returning the result to the client.
      </p>
      <button type="button" onClick={handleRefresh} disabled={isPending}>
        {isPending ? "Loading flag…" : "Invoke server function"}
      </button>
      {flag ? (
        <pre style={{ marginTop: "1rem", background: "#111", color: "#0f0", padding: "1rem" }}>
          {JSON.stringify({ enabled: flag.enabled, payload: flag.payload }, null, 2)}
        </pre>
      ) : (
        <p style={{ marginTop: "1rem" }}>No flag fetched yet.</p>
      )}
    </section>
  );
}

You can also use server actions in server components directly:

app/server-actions/page.tsx
import { fetchFlags } from "@basestack/flags-react/server";
import { ServerActionDemo } from "./demo";

export default async function ServerActionsPage() {
  const flags = await fetchFlags({
    baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
    projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
    environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
  });

  return (
    <main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
      <h1>Server Functions / Server Actions</h1>
      <p>This page renders entirely on the server and lists the current flags.</p>
      <ul>
        {flags.map((flag) => (
          <li key={flag.slug}>
            <strong>{flag.slug}</strong>: {flag.enabled ? "enabled" : "disabled"}
          </li>
        ))}
      </ul>
      <ServerActionDemo />
    </main>
  );
}

API Route Handler

API routes in the App Router use the Route Handler pattern. You can fetch flags server-side and return them as JSON responses. This is useful for creating REST endpoints that expose flag data.

app/api/flags/route.ts
import { NextResponse } from "next/server";
import { fetchFlag } from "@basestack/flags-react/server";
import { flagsConfig } from "@/libs/feature-flags/config";

export async function GET() {
  try {
    const flag = await fetchFlag("header", flagsConfig); 
    // flag.enabled, flag.payload, etc.
    return NextResponse.json({ flag });
  } catch (error) {
    console.error("Failed to load flag", error);
    return NextResponse.json({ message: "Unable to load flag" }, { status: 500 });
  }
}

Pages Router Implementation

The Pages Router is Next.js's original routing system. While the App Router is recommended for new projects, the Pages Router is still fully supported and widely used.

Setup the Configuration

Create a shared configuration file that can be used in both server and client contexts:

src/libs/feature-flags/server.ts
import type { SDKConfig } from "@basestack/flags-js";

export const flagsConfig: SDKConfig = {
  baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
  projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
  environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
};

Setup the App Component

In _app.tsx, wrap your application with the FlagsProvider. The provider accepts initialFlags from getServerSideProps or getStaticProps, which are used to hydrate the client-side state.

The preload prop is set to true only when no initial flags are provided, ensuring flags are fetched on the client if they weren't available during SSR.

pages/_app.tsx
import type { Flag } from "@basestack/flags-js";
import { FlagsProvider } from "@basestack/flags-react/client";
import type { AppProps } from "next/app";
import { flagsConfig } from "@/libs/feature-flags/config";

export default function MyApp({
  Component,
  pageProps,
}: AppProps<{ flags?: Flag[] }>) {
  const initialFlags = pageProps.flags ?? [];

  return (
    <FlagsProvider
      config={flagsConfig}
      initialFlags={initialFlags}
      preload={initialFlags.length === 0}
    >
      <Component {...pageProps} />
    </FlagsProvider>
  );
}

Using Flags in Pages

Fetch flags server-side using getServerSideProps or getStaticProps, then use the useFlag hook in your page component. This ensures flags are available immediately without client-side loading states.

pages/index.tsx
import type { Flag } from "@basestack/flags-js";
import { useFlag } from "@basestack/flags-react/client";
import { fetchFlags } from "@basestack/flags-react/server";
import type { GetServerSideProps } from "next";
import { flagsConfig } from "@/libs/feature-flags/config";

export default function HomePage({ flags }: { flags: Flag[] }) {
  // Client components can still call useFlag thanks to the provider in _app
  const { enabled, payload, isLoading } = useFlag<{ color?: string }>("header");

  return (
    <main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
      <h1>Next.js Pages Router Example</h1>
      <p>Server delivered {flags.length} flag(s).</p>
      <p>
        Flag <code>header</code> is{" "}
        {isLoading ? "loading" : enabled ? "enabled" : "disabled"}
      </p>
      {enabled && payload ? (
        <p>Payload: {JSON.stringify(payload, null, 2)}</p>
      ) : null}
    </main>
  );
}

export const getServerSideProps: GetServerSideProps<{
  flags: Flag[];
}> = async () => {
  const flags = await fetchFlags(flagsConfig);

  return {
    props: { flags },
  };
};

API Route

Pages Router API routes use a different pattern than App Router. Here's how to create an API endpoint that fetches flags:

pages/api/flags.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { fetchFlags } from "@basestack/flags-react/server";
import { flagsConfig } from "@/libs/feature-flags/config";

export default async function handler(
  _req: NextApiRequest,
  res: NextApiResponse,
) {
  try {
    const flags = await fetchFlags(flagsConfig);
    res.status(200).json({ flags });
  } catch (error) {
    console.error("Failed to load flags", error);
    res.status(500).json({ message: "Unable to load flags" });
  }
}

Understanding SSR and Hydration

When using Next.js with Basestack, flags are fetched on the server during the initial render. This provides several benefits:

  1. No Loading States: Flags are available immediately when components render
  2. SEO Friendly: Search engines can see the correct content based on flag states
  3. Performance: Reduces client-side API calls and improves Time to Interactive (TTI)
  4. Consistency: Server and client render the same content, preventing hydration mismatches

The FlagsHydrationScript (App Router) and initialFlags prop ensure that the client receives the same flag values that were used during server-side rendering.

Best Practices

1. Fetch Flags at the Root Level

Fetch flags in your root layout (app/layout.tsx) or _app.tsx to ensure they're available throughout your application. This prevents multiple API calls and ensures consistent flag values.

2. Use TypeScript Generics for Payloads

Type your flag payloads for better type safety and developer experience:

const { enabled, payload } = useFlag<{ variant: string; color: string }>("header");

3. Handle Loading States

While flags are typically available immediately due to SSR, handle loading states for better UX:

const { enabled, isLoading } = useFlag("header");

if (isLoading) return <LoadingSpinner />;

4. Error Handling

Always handle potential errors when fetching flags:

try {
  const flags = await fetchFlags(flagsConfig);
} catch (error) {
  console.error("Failed to fetch flags", error);
  // Fallback to default behavior
}

5. Cache Considerations

The SDK includes built-in caching. Flags are cached for 5 minutes by default, reducing unnecessary API calls. You can configure cache settings in the SDK config if needed.

Troubleshooting

Flags Not Updating

If flags aren't updating as expected:

  1. Check Cache: Flags are cached for 5 minutes. Wait for the cache to expire or clear it programmatically
  2. Verify Environment Variables: Ensure NEXT_PUBLIC_ prefixed variables are set correctly
  3. Check Network: Verify your application can reach the Basestack API endpoint

Hydration Mismatches

If you see hydration warnings:

  1. Ensure FlagsHydrationScript is included: In App Router, make sure FlagsHydrationScript is in your root layout
  2. Check initialFlags prop: Ensure flags fetched server-side are passed to the provider
  3. Avoid Client-Only Logic: Don't conditionally render different content based on flags that differ between server and client

Components Rendering Twice

If your components are rendering twice, this is likely due to React.StrictMode in development. This is expected behavior and helps identify potential issues. In production, components will only render once.

Environment Variables Not Available

If environment variables aren't available:

  1. Check Prefix: Use NEXT_PUBLIC_ prefix for variables needed in client components
  2. Restart Dev Server: After adding environment variables, restart your Next.js dev server
  3. Verify .env File: Ensure your .env file is in the project root

API Reference - Props & Parameters

Components

FlagsProvider

PropTypeRequiredDefaultDescription
configSDKConfigYes-Configuration object for the Flags SDK
childrenReactNodeYes-React children to be wrapped by the provider
initialFlagsFlag[]No[]Initial flags to populate the provider with
preloadbooleanNotrueWhether to automatically preload flags on mount
onError(error: Error) => voidNo-Callback function called when an error occurs

FlagsHydrationScript

PropTypeRequiredDefaultDescription
flagsFlag[]Yes-Array of flags to hydrate on the client
idstringNo"basestack-flags-hydration"HTML id attribute for the script tag
noncestringNo-Content Security Policy nonce for the script tag
globalKeystringNo"__BASESTACK_FLAGS__"Global window key to store hydrated flags

Hooks

useFlag<TPayload>(slug, options?)

Parameters:

ParameterTypeRequiredDefaultDescription
slugstringYes-The flag slug to retrieve
optionsUseFlagOptions<TPayload>No-Options object (see below)

Options (UseFlagOptions):

OptionTypeRequiredDefaultDescription
defaultEnabledbooleanNofalseDefault enabled state if flag is not loaded
defaultPayloadTPayloadNo-Default payload value if flag is not loaded
fetchbooleanNotrueWhether to automatically fetch the flag if not cached

Returns (UseFlagResult<TPayload>):

PropertyTypeDescription
flagFlag | undefinedThe flag object if available
enabledbooleanWhether the flag is enabled
payloadTPayload | undefinedThe flag payload
isLoadingbooleanWhether the flag is currently loading
errorError | nullAny error that occurred during fetching
refresh() => Promise<Flag | undefined>Function to manually refresh the flag

useFlags()

Parameters: None

Returns:

PropertyTypeDescription
flagsFlag[]Array of all flags
flagsBySlugRecord<string, Flag>Object mapping flag slugs to flag objects
isLoadingbooleanWhether flags are currently loading
errorError | nullAny error that occurred during fetching
refresh() => Promise<void>Function to manually refresh all flags

useFlagsClient()

Parameters: None

Returns:

PropertyTypeDescription
(client)FlagsSDKThe Flags SDK client instance

useFlagsContext()

Parameters: None

Returns:

PropertyTypeDescription
clientFlagsSDKThe Flags SDK client instance
flagsMap<string, Flag>Map of flags by slug
loadingbooleanWhether flags are currently loading
errorError | nullAny error that occurred
refresh() => Promise<void>Function to manually refresh all flags
upsertFlag(flag: Flag) => voidFunction to update or insert a flag

Server Functions

createServerFlagsClient(config)

ParameterTypeRequiredDescription
configSDKConfigYesConfiguration object for the Flags SDK

Returns: FlagsSDK - A new Flags SDK client instance

fetchFlag(slug, config)

ParameterTypeRequiredDescription
slugstringYesThe flag slug to fetch
configSDKConfigYesConfiguration object for the Flags SDK

Returns: Promise<Flag> - The fetched flag

fetchFlags(config, slugs?)

ParameterTypeRequiredDescription
configSDKConfigYesConfiguration object for the Flags SDK
slugsstring[]NoOptional array of flag slugs to fetch. If not provided, fetches all flags

Returns: Promise<Flag[]> - Array of fetched flags

Utility Functions

readHydratedFlags(globalKey?)

ParameterTypeRequiredDefaultDescription
globalKeystringNo"__BASESTACK_FLAGS__"Global window key to read hydrated flags from

Returns: Flag[] | undefined - Array of hydrated flags, or undefined if not available (e.g., on server)

Types

SDKConfig

Exported from @basestack/flags-js - Configuration for the Flags SDK

Flag

Exported from @basestack/flags-js - Flag object type

CacheConfig

Exported from @basestack/flags-js - Cache configuration type