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

# Next.js example

> Build a Next.js app that signs a user in with Nemu and calls the API as them

This walks through a complete Next.js app: the user signs in with Nemu, your server holds the
tokens, and your pages call the management API as that user.

<Note>
  The SDK is server side only, and for one reason: it holds your `client_secret`. There is no
  React component, hook or provider to import, and nothing to render. Data reaches the browser as
  plain props from a server component, which is what the dashboard below does.

  It has no Node specific imports. Everything cryptographic uses the standard Web Crypto API, so
  it runs anywhere with `globalThis.crypto`: Node, Bun, Deno, the Edge runtime and Workers.
</Note>

## Register the application

Open the console, go to Settings, then Applications, and create a **Web application**.

<Steps>
  <Step title="Add the redirect URI">
    It must match what your app sends, exactly. For local development on port 9999 that is
    `http://localhost:9999/api/auth/callback`.
  </Step>

  <Step title="Select scopes">
    This example asks for `profile:read`, `workspaces:read`, `models:read` and `usage:read`. A
    token carries only the intersection of what the user approved and what the application holds,
    so a scope you skip here fails later even though the user consented.
  </Step>

  <Step title="Copy the credentials">
    You get a `client_id` and a `client_secret`. The secret is shown once and belongs on your
    server.
  </Step>
</Steps>

## Create the app

```bash theme={null}
npx create-next-app@latest nemu-oauth --typescript --app
cd nemu-oauth
npm install @nemu-ai/sdk
```

Set the environment. `SESSION_SECRET` encrypts the cookie holding the tokens, so generate a real
one and keep it stable.

```bash .env theme={null}
NEMU_CLIENT_ID=
NEMU_CLIENT_SECRET=
NEMU_REDIRECT_URI=http://localhost:9999/api/auth/callback
SESSION_SECRET=
```

```bash theme={null}
node -e "console.log(Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex'))"
```

## The OAuth client

Keep one place that builds the client and names the scopes. `build_scopes` validates as it
builds, so a typo throws here instead of turning into a `403` on an unrelated request later.

```ts lib/oauth.ts theme={null}
import { NemuOAuth, build_scopes, type Scope } from "@nemu-ai/sdk";

export const SCOPES: Scope[] = build_scopes({
  profile: "read",
  workspaces: "read",
  models: "read",
  usage: "read",
});

export function create_oauth(): NemuOAuth {
  return new NemuOAuth({
    client_id: process.env.NEMU_CLIENT_ID!,
    client_secret: process.env.NEMU_CLIENT_SECRET!,
    redirect_uri: process.env.NEMU_REDIRECT_URI!,
  });
}
```

## Send the user to consent

`authorize_url` generates the PKCE pair for you. The verifier and the state have to survive the
round trip, so store them in a short lived httpOnly cookie and read them back in the callback.

```ts app/api/auth/login/route.ts theme={null}
import { NextResponse } from "next/server";
import { create_oauth, SCOPES } from "@/lib/oauth";
import { write_pending } from "@/lib/session";

export async function GET() {
  const state = crypto.randomUUID();
  const { url, code_verifier } = await create_oauth().authorize_url({
    scopes: SCOPES,
    state,
  });

  await write_pending({ code_verifier, state });
  return NextResponse.redirect(url);
}
```

## Handle the callback

Compare the returned `state` against the one you stored before exchanging anything. `exchange_code`
returns a ready client, and `nemu.tokens` is what you persist.

```ts app/api/auth/callback/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { create_oauth } from "@/lib/oauth";
import { take_pending, write_session } from "@/lib/session";

export async function GET(request: NextRequest) {
  const params = request.nextUrl.searchParams;
  const code = params.get("code");
  const state = params.get("state");
  if (code === null || state === null) {
    return NextResponse.redirect(new URL("/?error=missing_code", request.url));
  }

  const pending = await take_pending();
  if (pending === null || pending.state !== state) {
    return NextResponse.redirect(new URL("/?error=state_mismatch", request.url));
  }

  const nemu = await create_oauth().exchange_code({
    code,
    code_verifier: pending.code_verifier,
  });

  await write_session(nemu.tokens!);
  return NextResponse.redirect(new URL("/dashboard", request.url));
}
```

## Refresh in the right place

<Warning>
  Refresh tokens rotate, and using one revokes it immediately. If a refresh happens somewhere its
  result cannot be saved, the old token is already dead and the next request signs the user out
  with nothing to explain it.
</Warning>

A server component cannot write cookies in Next.js. So the refresh must not happen during a page
render. Put it in `proxy.ts`, which runs before the page and can write to the response.

```ts proxy.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import type { OAuthTokens } from "@nemu-ai/sdk";
import { create_oauth } from "@/lib/oauth";
import { needs_refresh } from "@/lib/client";
import { SESSION_COOKIE, SESSION_COOKIE_OPTIONS, seal, unseal } from "@/lib/session";

export const config = {
  matcher: ["/dashboard/:path*"],
};

export async function proxy(request: NextRequest) {
  const raw = request.cookies.get(SESSION_COOKIE)?.value;
  if (raw === undefined) return NextResponse.next();

  const tokens = await unseal<OAuthTokens>(raw);
  if (tokens === null || !needs_refresh(tokens)) return NextResponse.next();

  const rotated = await create_oauth().from_tokens(tokens).refresh();
  const response = NextResponse.next();
  response.cookies.set(
    SESSION_COOKIE,
    await seal(rotated),
    SESSION_COOKIE_OPTIONS(),
  );
  return response;
}
```

Then make it impossible for a stale token to reach the SDK at all. If the token is close to
expiry, return nothing and let the page redirect to sign in.

```ts lib/client.ts theme={null}
import type { Nemu, OAuthTokens } from "@nemu-ai/sdk";
import { create_oauth } from "@/lib/oauth";
import { read_session } from "@/lib/session";

export const REFRESH_WINDOW_MS = 120_000;

export function needs_refresh(tokens: OAuthTokens): boolean {
  if (tokens.expires_at === undefined) return true;
  return Date.now() >= tokens.expires_at - REFRESH_WINDOW_MS;
}

export async function get_client(): Promise<Nemu | null> {
  const tokens = await read_session();
  if (tokens === null) return null;
  if (needs_refresh(tokens)) return null;
  return create_oauth().from_tokens(tokens);
}
```

## Call the API

The page is a server component. It calls the API and hands plain data to the interface, so no
token is ever serialized to the browser.

```tsx app/dashboard/page.tsx theme={null}
import { redirect } from "next/navigation";
import { get_client } from "@/lib/client";
import { Content } from "./content";

export default async function DashboardPage() {
  const nemu = await get_client();
  if (nemu === null) redirect("/api/auth/login");

  const [user, workspaces] = await Promise.all([
    nemu.user.get(),
    nemu.workspaces.list(),
  ]);

  return (
    <Content
      user={user.toJSON()}
      workspaces={workspaces.map((workspace) => workspace.toJSON())}
    />
  );
}
```

`toJSON()` strips the entity's methods and internal client, leaving a plain object that crosses
the server and client boundary cleanly.

## Storing the session

The example seals tokens into an httpOnly cookie with AES-256-GCM, using Web Crypto and a key
derived with PBKDF2 from `SESSION_SECRET`. It keeps the whole thing in one file with no
database, which is what makes it easy to read. A realistic token pair seals to roughly 800
bytes.

<Warning>
  A cookie is a reasonable default and a poor ceiling. It caps at roughly 4KB, it travels on every
  request, and revoking one session means waiting for it to expire. Move to a server side session
  store once you have real users.
</Warning>

Whatever you store it in, the rules do not change. Tokens and the `client_secret` stay on the
server, cookies are `httpOnly` and `secure` in production, and `sameSite` is `lax` so the cookie
survives the redirect back from consent.

## Run it

```bash theme={null}
npm run dev -- -p 9999
```

Open `http://localhost:9999`, sign in, and the dashboard shows your account and workspaces read
straight from the API.

## The full example

The complete app is in the repository under `others/examples/oauth-sdk-nextjs`, including the
sealed cookie implementation, the error states on the sign in screen, and the design tokens.

## Prefer Auth.js

If your app already uses Auth.js, or you want the session handling solved for you, there is a
second example that wires Nemu in as a custom Auth.js provider instead of doing the flow by
hand.

<CardGroup cols={2}>
  <Card title="All examples" href="/examples">
    Both example apps, and which one to start from.
  </Card>

  <Card title="Scopes" href="/oauth/scopes">
    Every scope, the aliases, and the rest of what `build_scopes` accepts.
  </Card>
</CardGroup>
