> ## 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.

# Examples

> Complete apps you can run, showing how to sign a user in and call the API

Two working Next.js apps, both in the repository under `others/examples`. They do the same
thing and differ only in who owns the session.

<CardGroup cols={2}>
  <Card title="Plain SDK" href="/oauth/example" icon="key">
    Runs the OAuth flow yourself with `NemuOAuth`. No auth library. Around 12 files, and nothing
    hidden.
  </Card>

  <Card title="Auth.js" icon="shield-check">
    Registers Nemu as a custom Auth.js v5 provider. Auth.js owns the session, the cookies and the
    refresh.
  </Card>
</CardGroup>

## Which one

|                 | Plain SDK                                               | Auth.js                                                  |
| --------------- | ------------------------------------------------------- | -------------------------------------------------------- |
| Directory       | `others/examples/oauth-sdk-nextjs`                      | `others/examples/next-auth-sdk-nextjs`                   |
| Dependencies    | The SDK                                                 | The SDK plus `next-auth@beta`                            |
| Session storage | An AES-256-GCM sealed cookie you own                    | Auth.js encrypted JWT cookie                             |
| Token refresh   | In `proxy.ts`, written by you                           | In the `jwt` callback                                    |
| Redirect URI    | `/api/auth/callback`                                    | `/api/auth/callback/nemu`                                |
| Good when       | Nemu is the only sign in, or you want to see every step | The app already uses Auth.js, or will add more providers |

Start with the plain SDK example if you are learning the flow. Reach for Auth.js if you already
have it, or expect to add Google or GitHub alongside Nemu later.

Both run on port `9999`, so the redirect URI does not collide with whatever else is on 3000.

## Auth.js as a custom provider

Nemu is a plain OAuth 2.0 provider, not OpenID Connect, so it is registered by hand rather than
through discovery.

```ts lib/provider.ts theme={null}
import type { OAuthConfig } from "next-auth/providers";
import { format_scopes } from "@nemu-ai/sdk";

const origin = "https://app.nemu.cc";

export const SCOPE = format_scopes({
  profile: "read",
  workspaces: "read",
  models: "read",
  usage: "read",
});

export const nemu_provider: OAuthConfig<NemuProfile> = {
  id: "nemu",
  name: "Nemu",
  type: "oauth",
  clientId: process.env.AUTH_NEMU_ID,
  clientSecret: process.env.AUTH_NEMU_SECRET,
  authorization: { url: `${origin}/oauth/authorize`, params: { scope: SCOPE } },
  token: `${origin}/api/oauth/token`,
  userinfo: `${origin}/api/v1/user`,
  checks: ["pkce", "state"],
  client: { token_endpoint_auth_method: "client_secret_basic" },
  profile(profile) {
    return {
      id: profile.data.id,
      name: profile.data.username,
      email: profile.data.email,
    };
  },
};
```

<Warning>
  Three of those fields override an Auth.js default that is wrong for a non-OIDC provider.

  `checks` defaults to `["pkce"]` alone, so `state` is not sent unless you ask for it.

  Given `authorization` as a bare URL string, Auth.js appends `scope=openid profile email`, even
  for a `type: "oauth"` provider. Nemu has no such scopes, so consent fails. Always pass
  `{ url, params: { scope } }`.

  `userinfo` is required even for OAuth 2.0, because Auth.js always fetches a profile. Nemu wraps
  its response in `data`, so `profile()` has to unwrap it or every user gets an undefined id.
</Warning>

The redirect URI Auth.js will use is `{origin}/api/auth/callback/{id}`, so for `id: "nemu"` you
register `http://localhost:9999/api/auth/callback/nemu`. That is a different URI from the plain
SDK example.

## Refreshing with Auth.js

Refresh belongs in the `jwt` callback, because that is where Auth.js re-issues the session
cookie on the same response and the rotated token actually gets saved.

```ts auth.ts theme={null}
async jwt({ token, account }) {
  if (account) {
    return {
      ...token,
      access_token: account.access_token,
      refresh_token: account.refresh_token,
      expires_at: account.expires_at,
      scope: account.scope,
    };
  }

  if (Date.now() / 1000 < (token.expires_at ?? 0) - 120) return token;
  if (token.refresh_token === undefined) {
    return { ...token, error: "RefreshTokenError" as const };
  }

  try {
    const rotated = await create_oauth()
      .from_tokens({
        access_token: token.access_token ?? "",
        refresh_token: token.refresh_token,
      })
      .refresh();

    return {
      ...token,
      access_token: rotated.access_token,
      refresh_token: rotated.refresh_token ?? token.refresh_token,
      expires_at: Math.floor((rotated.expires_at ?? 0) / 1000),
      error: undefined,
    };
  } catch {
    return { ...token, error: "RefreshTokenError" as const };
  }
}
```

<Warning>
  Refresh tokens rotate, and using one revokes the old one immediately. This pattern has a known
  race, acknowledged in the Auth.js documentation: two concurrent requests on an expired token
  both refresh, and the loser is left holding a revoked token. Refreshing early shrinks the
  window. A shared lock or a database session with a single writer closes it.
</Warning>

Two details that cost people an afternoon:

`account` is only present on the first call, at sign in. Every later call has to work from what
you already stored on the token.

`account.expires_at` is in **seconds**, and the SDK's `expires_at` is in **milliseconds**.
Convert at the boundary, in both directions.

## Type augmentation

Auth.js has no generics for this. Custom session and token fields are declared by augmenting
its modules, and the declaration has to live in a file TypeScript already includes.

```ts auth.ts theme={null}
import type { JWT } from "next-auth/jwt";

declare module "next-auth" {
  interface Session {
    access_token?: string;
    scope?: string;
    error?: "RefreshTokenError";
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    access_token?: string;
    refresh_token?: string;
    expires_at?: number;
    scope?: string;
    error?: "RefreshTokenError";
  }
}
```

<Note>
  That `import type { JWT }` looks unused and is not. TypeScript cannot augment a module it has
  not resolved, so dropping it turns every field on the token back into `unknown` without any
  error pointing at the cause.
</Note>

## Keep the token off the client

The session carries `access_token` so server code can reach the API. That is only safe because
neither example mounts a `SessionProvider` or calls `useSession`, so the session is never
serialized into a client component.

If you add client side session access, take the token out of the session first and read it
server side instead.
