> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useshipd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js example

> A complete Next.js App Router app that evaluates shipd flags in server components and client hooks.

## Overview

A complete Next.js App Router app. A server component evaluates `new-checkout` with a server SDK key. A client component reads `search-variant` with typed React hooks and a client SDK key.

## What's in it

* `src/lib/shipd-server.ts` holds one server client per process. `import "server-only"` keeps the server key out of the browser bundle.
* `src/lib/shipd-react.ts` binds typed hooks once with `createShipdReact<FlagDefinitions>()`.
* `src/app/providers.tsx` creates the browser client with `NEXT_PUBLIC_SHIPD_SDK_KEY` and calls `identify` for the signed-in user.
* `src/app/page.tsx` evaluates `new-checkout` on the server. `src/components/search-box.tsx` evaluates `search-variant` in the browser.

```text theme={null}
nextjs-example/
├── .env.example
├── next.config.ts
├── package.json
├── shipd.config.ts
├── tsconfig.json
└── src/
    ├── app/
    │   ├── layout.tsx
    │   ├── page.tsx
    │   └── providers.tsx
    ├── components/
    │   └── search-box.tsx
    ├── generated/
    │   └── flags.ts
    └── lib/
        ├── session.ts
        ├── shipd-react.ts
        └── shipd-server.ts
```

`next.config.ts`, `tsconfig.json`, and `next-env.d.ts` are the `create-next-app` defaults, including the `@/*` → `./src/*` path alias.

## Files

```json package.json theme={null}
{
  "name": "nextjs-example",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "flags:diff": "shipd diff",
    "flags:push": "shipd push"
  },
  "dependencies": {
    "next": "^16.0.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "server-only": "^0.0.1",
    "shipdit": "^0.3.0"
  },
  "devDependencies": {
    "@shipdit/cli": "^0.3.2",
    "@types/node": "^22.18.0",
    "@types/react": "^19.2.0",
    "@types/react-dom": "^19.2.0",
    "typescript": "^5.9.0"
  }
}
```

```ts shipd.config.ts theme={null}
import { defineFlags } from "shipdit/config";

export default defineFlags({
  "new-checkout": {
    type: "boolean",
    default: false,
    description: "Redesigned checkout flow",
    tags: ["checkout"],
  },
  "search-variant": {
    type: "variant",
    variants: ["control", "semantic", "hybrid"] as const,
    default: "control",
    description: "Search ranking experiment",
    tags: ["search", "experiment"],
  },
});
```

```bash .env.example theme={null}
# Server SDK key (server components, route handlers). Never prefix with NEXT_PUBLIC_.
SHIPD_SDK_KEY=shipd_sdk_server_...
# Client SDK key (browser). Safe to embed.
NEXT_PUBLIC_SHIPD_SDK_KEY=shipd_sdk_client_...
```

`shipd push` and `shipd types gen` write this file. Commit it and do not edit it by hand.

```ts src/generated/flags.ts theme={null}
/* eslint-disable */
/* This file is generated by shipd types gen. Do not edit. */

export interface FlagDefinitions {
  "new-checkout": {
    type: "boolean";
    default: false;
  };

  "search-variant": {
    type: "variant";
    variants: "control" | "semantic" | "hybrid";
    default: "control";
  };
}

export type FlagKey = keyof FlagDefinitions;

export type BooleanFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "boolean" ? K : never;
}[FlagKey];

export type VariantFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "variant" ? K : never;
}[FlagKey];

export type NumberFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "number" ? K : never;
}[FlagKey];

export type JsonFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "json" ? K : never;
}[FlagKey];
```

```ts src/lib/shipd-server.ts theme={null}
import "server-only";

import { createClient, type ShipdClient } from "shipdit";

import type { FlagDefinitions } from "@/generated/flags";

function createShipd(): ShipdClient<FlagDefinitions> {
  return createClient<FlagDefinitions>({
    sdkKey: process.env.SHIPD_SDK_KEY!,
    stream: true,
    defaults: {
      "new-checkout": false,
      "search-variant": "control",
    },
  });
}

// Reuse one client across hot reloads in development.
const globalForShipd = globalThis as unknown as { shipd?: ShipdClient<FlagDefinitions> };

export const shipd = globalForShipd.shipd ?? createShipd();

if (process.env.NODE_ENV !== "production") {
  globalForShipd.shipd = shipd;
}
```

```ts src/lib/shipd-react.ts theme={null}
"use client";

import { createShipdReact } from "shipdit/client/react";

import type { FlagDefinitions } from "@/generated/flags";

export const { ShipdProvider, useFlag, useVariant, useNumber, useJson, useReady, useShipdClient } =
  createShipdReact<FlagDefinitions>();
```

Replace `getUserId` with your auth library. This example reads a `uid` cookie.

```ts src/lib/session.ts theme={null}
import { cookies } from "next/headers";

export async function getUserId(): Promise<string | undefined> {
  const cookieStore = await cookies();
  return cookieStore.get("uid")?.value;
}
```

```tsx src/app/providers.tsx theme={null}
"use client";

import { useEffect, useRef, type ReactNode } from "react";
import { createClient } from "shipdit/client";

import type { FlagDefinitions } from "@/generated/flags";
import { ShipdProvider } from "@/lib/shipd-react";

const isBrowser = typeof window !== "undefined";

// Module scope: one client per browser tab. Next.js also evaluates this module
// once during server rendering, so that copy fetches once and skips polling,
// the stream, and events.
const client = createClient<FlagDefinitions>({
  sdkKey: process.env.NEXT_PUBLIC_SHIPD_SDK_KEY!,
  stream: isBrowser,
  refreshIntervalMs: isBrowser ? 30_000 : 0,
  events: isBrowser,
  defaults: {
    "new-checkout": false,
    "search-variant": "control",
  },
});

export function Providers({ userId, children }: { userId?: string; children: ReactNode }) {
  const previousUserId = useRef<string | undefined>(undefined);

  useEffect(() => {
    if (userId) {
      client.identify({ userId });
    } else if (previousUserId.current) {
      // Signed out: clear the identity and start a new anonymous id.
      client.reset();
    }
    previousUserId.current = userId;
  }, [userId]);

  return <ShipdProvider client={client}>{children}</ShipdProvider>;
}
```

```tsx src/app/layout.tsx theme={null}
import type { ReactNode } from "react";

import { getUserId } from "@/lib/session";

import { Providers } from "./providers";

export default async function RootLayout({ children }: { children: ReactNode }) {
  const userId = await getUserId();

  return (
    <html lang="en">
      <body>
        <Providers userId={userId}>{children}</Providers>
      </body>
    </html>
  );
}
```

```tsx src/app/page.tsx theme={null}
import { SearchBox } from "@/components/search-box";
import { getUserId } from "@/lib/session";
import { shipd } from "@/lib/shipd-server";

export default async function Page() {
  const userId = await getUserId();

  // Resolves after the first snapshot, or after 5s with `defaults`. Never rejects.
  await shipd.ready();
  const newCheckout = shipd.isEnabled("new-checkout", { userId });

  return (
    <main>
      <h1>Store</h1>
      <SearchBox />
      <a href={newCheckout ? "/checkout/v2" : "/checkout"}>
        {newCheckout ? "Try the new checkout" : "Checkout"}
      </a>
    </main>
  );
}
```

```tsx src/components/search-box.tsx theme={null}
"use client";

import { useReady, useVariant } from "@/lib/shipd-react";

const placeholders = {
  control: "Search products",
  semantic: "Describe what you're looking for",
  hybrid: "Search by name or description",
} as const;

export function SearchBox() {
  const ready = useReady();
  // "control" | "semantic" | "hybrid"
  const variant = useVariant("search-variant");

  // useReady() is false during server rendering and the first client render,
  // so the markup matches and hydration succeeds.
  if (!ready) {
    return <input type="search" placeholder="Search" disabled />;
  }

  return (
    <form action="/search" data-search-variant={variant}>
      <input type="search" name="q" placeholder={placeholders[variant]} />
    </form>
  );
}
```

## Run it

```bash theme={null}
pnpm install
cp .env.example .env.local  # paste a server key and a client key
pnpm exec shipd login       # approve org → project → environment in the browser
pnpm exec shipd push        # creates the flags and rewrites src/generated/flags.ts
pnpm dev
```

Open `http://localhost:3000`. To test targeting, set a `uid` cookie (for example `usr_8f2k19`) in your browser's dev tools. Pushed flags serve their `default`. Add a rule for `new-checkout` in the dashboard, click **Save environment**, and reload.

## Adapt it

* Server components are rendered per request. Pass the evaluation context on every call (`{ userId, attributes }`). Don't call `identify` on the shared server client.
* Put the browser user's traits in `identify({ userId, traits: { plan } })`. Hooks re-render when context or the snapshot changes.
* The browser client adds `country`, `region`, `city`, and `timezone` attributes automatically. The server client does not. If a rule targets location, pass those attributes on the server yourself.
* For route handlers and server actions, import `shipd` from `@/lib/shipd-server` in the same way.

## Next

* [Next.js SDK guide](/sdks/nextjs)
* [React SDK guide](/sdks/react)
* [SDK keys](/concepts/keys)
* [SDK reference](/reference/sdk)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.