# Next.js (https://session-kit.pages.dev/docs/setups/next)

In this guide, you keep the session in an encrypted cookie with the Next.js
App Router. A Server Function signs in, a session route answers the browser,
Proxy refreshes the tokens, and Server Components read the session. The first
response already shows the signed-in page, so the browser never shows a
"Checking session…" state or a flash of signed-out content. Client
Components get a small client that mirrors the session, and the
[hooks](https://session-kit.pages.dev/docs/getting-started#4-read-the-session-in-react) work with it.
[Server rendering](https://session-kit.pages.dev/docs/server-rendering) explains how the pieces fit.

A complete app is in
[`examples/next-app`](https://github.com/monarcode/session-kit/tree/main/examples/next-app).

## Before you begin

- Next.js 16, with the App Router.
- A secret of 32 or more random characters to seal the cookie, set as
  `SESSION_SECRET` on the server. To make one, run
  `openssl rand -base64 32`. `next build` doesn't need it.

## Installation

To install the packages, run the following command:

```npm
npm install @monarcode/session-kit next zod
```

## 1. Create the server engine

`createServerAuth` from `@monarcode/session-kit/server` keeps the session in
a cookie sealed with your `secret`, and refreshes it with your backend. Create
it in a server-only module, so Next refuses to bundle it for the browser:

```ts title="lib/auth/server.ts"
import "server-only";
import { createServerAuth } from "@monarcode/session-kit/server";
import { z } from "zod";

const API = "https://api.example.com";

const userSchema = z.object({ id: z.number(), firstName: z.string() });
const tokens = z.object({ accessToken: z.string(), refreshToken: z.string() });

export const serverAuth = createServerAuth({
  name: "app",
  user: userSchema,
  secret: process.env.SESSION_SECRET!,
  refresh: async ({ refreshToken, signal }) => {
    const response = await fetch(`${API}/auth/refresh`, {
      method: "POST",
      signal,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });
    // The backend rejected the refresh token: sign out.
    if (response.status === 401) return null;
    if (!response.ok) throw new Error(`Refresh failed (${response.status})`);
    return tokens.parse(await response.json());
  },
});

/** Signs in with your backend, from the server. */
export async function login(username: string, password: string) {
  const response = await fetch(`${API}/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });
  const body: unknown = await response.json().catch(() => null);
  if (!response.ok) {
    // The backend says why, such as wrong credentials, in `message`.
    const failure = z.object({ message: z.string() }).safeParse(body);
    throw new Error(
      failure.success
        ? failure.data.message
        : `Sign-in failed (${response.status})`,
    );
  }
  return { ...tokens.parse(body), user: userSchema.parse(body) };
}
```

[Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions#the-cookie) covers
its other options, rotating the secret, signing out, and the limits every
setup shares.

## 2. Read the session in Server Components

`nextSession` from `@monarcode/session-kit/next` gives your app its session
over Next's cookies. Session kit doesn't import `next`, so pass it Next's own
`cookies` and `redirect`. Create it in a server-only module too:

```ts title="lib/auth/session.ts"
import "server-only";
import { nextSession } from "@monarcode/session-kit/next";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { serverAuth } from "./server";

export const session = nextSession(serverAuth, { cookies, redirect });
```

In a Server Component, `await session.get()` resolves `{ sessionId, user }`
or `null`. It holds no tokens, so it's safe to pass to Client Components. It
doesn't set cookies, as Server Components can't. Proxy refreshes first.

## 3. Refresh in Proxy

Proxy refreshes access that is about to expire before any page or Server
Function reads it. `session.refresh(request, response)` sets the new cookie
on the response, and Next passes it on to the Server Components rendering
the same request:

```ts title="proxy.ts"
import { NextResponse, type NextRequest } from "next/server";

import { session } from "./lib/auth/session";

export function proxy(request: NextRequest) {
  return session.refresh(request, NextResponse.next());
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

If the refresh fails, the page still renders with the cookie as it is.
Server Functions are POST requests to the page that calls them, so keep the
matcher wide enough to include those pages.

## 4. Add the session route and sign-in

The browser's client reads the session and signs out through a session
route. `sessionRoute` from `@monarcode/session-kit/next` makes its `GET` and
`POST` handlers. Pass it Next's `cookies`:

```ts title="app/api/session/route.ts"
import { sessionRoute } from "@monarcode/session-kit/next";
import { cookies } from "next/headers";

import { serverAuth } from "../../../lib/auth/server";

export const { GET, POST } = sessionRoute(serverAuth, { cookies });
```

It answers your own site's pages only, so another site can't sign the user
out: see [Security](https://session-kit.pages.dev/docs/security).

Signing in is yours, as it takes your form's input. A Server Function sets
the cookie with `session.signIn`:

```ts title="app/actions.ts"
"use server";

import { z } from "zod";

import { login } from "../lib/auth/server";
import { session } from "../lib/auth/session";

const credentials = z.object({ username: z.string(), password: z.string() });

export async function signInAction(input: unknown) {
  const { username, password } = credentials.parse(input);
  try {
    const tokens = await login(username, password);
    return { session: await session.signIn(tokens) };
  } catch (error) {
    return {
      error: error instanceof Error ? error.message : "Sign-in failed",
    };
  }
}
```

`signInAction` returns its failure rather than throwing it, as Next advises
for expected errors. In production, Next hides a thrown error's message from
the browser.

## 5. Create the browser client

`createMirrorAuth` makes the client that Client Components use, from the
session the layout read, the session route and your sign-in:

```ts title="lib/auth/client.ts"
import { createMirrorAuth, type SessionOf } from "@monarcode/session-kit";

import { signInAction } from "../../app/actions";
// Types only: left out of the browser's bundle.
import type { serverAuth } from "./server";

export type AppSession = SessionOf<typeof serverAuth>;

export function createAppAuth(session: AppSession | null) {
  return createMirrorAuth({
    name: "app",
    session,
    endpoint: "/api/session",
    signIn: async (input: { username: string; password: string }) => {
      const result = await signInAction(input);
      if ("error" in result) throw new Error(result.error);
      return result.session;
    },
  });
}

export type AppAuth = ReturnType<typeof createAppAuth>;
```

`useMirrorAuth` from `@monarcode/session-kit/react` creates the client in a
Client Component, once per request on the server, never at module level. It
keeps the client and the server in step both ways. When the session changes
in the browser, it calls `onChange`, here `router.refresh()`, which renders
Server Components again. Register the client's type where you render
`AuthProvider`, so the hooks carry your user:

```tsx title="app/providers.tsx"
"use client";

import type { ReactNode } from "react";

import { AuthProvider, useMirrorAuth } from "@monarcode/session-kit/react";
import { useRouter } from "next/navigation";

import {
  createAppAuth,
  type AppAuth,
  type AppSession,
} from "../lib/auth/client";

// Types `AuthProvider` and the hooks with this client.
declare module "@monarcode/session-kit" {
  interface Register {
    auth: AppAuth;
  }
}

export function Providers(props: {
  session: AppSession | null;
  children: ReactNode;
}) {
  const router = useRouter();
  const auth = useMirrorAuth({
    create: createAppAuth,
    session: props.session,
    onChange: () => router.refresh(),
  });
  return <AuthProvider client={auth}>{props.children}</AuthProvider>;
}
```

Render the providers in the root layout, with the session it reads:

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

import { session } from "../lib/auth/session";
import { Providers } from "./providers";

export default async function RootLayout(props: { children: ReactNode }) {
  const current = await session.get();
  return (
    <html lang="en">
      <body>
        <Providers session={current}>{props.children}</Providers>
      </body>
    </html>
  );
}
```

## 6. Protect pages

Check the session in each page with `session.require`, rather than in a
layout. Layouts don't render again on navigation. It takes the page's own
path, where the sign-in page sends the user back, as Server Components don't
know their URL:

```tsx title="app/page.tsx"
import { session } from "../lib/auth/session";

export default async function Home() {
  const { user } = await session.require("/");
  return <h1>Welcome, {user.firstName}</h1>;
}
```

The sign-in page sends a signed-in user on to `redirectTo` with
`session.redirectIfSignedIn`, and its form calls the client's `signIn`. When
the session changes in the browser, after a sign-in or sign-out here or in
another tab, `useMirrorAuth` renders the page again on the server, so these
redirects run without code in the form:

```tsx title="app/login/page.tsx"
import { session } from "../../lib/auth/session";
import { SignInForm } from "./sign-in-form";

export default async function Login({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  await session.redirectIfSignedIn((await searchParams).redirectTo);
  return <SignInForm />;
}
```

The form is a Client Component that calls the client's `signIn`:

```tsx title="app/login/sign-in-form.tsx"
"use client";

import { useState, type FormEvent } from "react";

import { useAuthClient } from "@monarcode/session-kit/react";

export function SignInForm() {
  const auth = useAuthClient();
  const [error, setError] = useState("");

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    try {
      await auth.signIn({
        username: String(form.get("username")),
        password: String(form.get("password")),
      });
    } catch (failure) {
      // What the client's signIn option threw, from signInAction's error.
      setError(failure instanceof Error ? failure.message : "Sign-in failed");
    }
  }

  return (
    <form onSubmit={submit}>
      <label>
        Username
        <input name="username" autoComplete="username" />
      </label>
      <label>
        Password
        <input
          name="password"
          type="password"
          autoComplete="current-password"
        />
      </label>
      {error && <p role="alert">{error}</p>}
      <button>Sign in</button>
    </form>
  );
}
```

In this guide, the server signs in with your backend, so the user in the cookie
comes from the backend. If the browser signs in with the backend itself, have
`signInAction` take the tokens instead, as
[Hybrid setup](https://session-kit.pages.dev/docs/server-rendering/hybrid) shows for TanStack Start.

## Call your API

Call your API with `createAuthFetch` from `@monarcode/session-kit/http`. It
sends the request's access token to your API's origin only. In Server
Components, pass it `session.readOnly()`, the request's session that doesn't
set cookies. Proxy refreshed the token before the render. A token that
expired anyway rejects with `REFRESH_FAILED`, and so does a 401 from your API,
since nothing new can be saved:

```tsx title="app/orders/page.tsx"
import { createAuthFetch } from "@monarcode/session-kit/http";
import { z } from "zod";

import { session } from "../../lib/auth/session";

export default async function Orders() {
  await session.require("/orders");
  const apiFetch = createAuthFetch(await session.readOnly(), {
    baseUrl: "https://api.example.com/",
  });
  const response = await apiFetch("orders");
  if (!response.ok) throw new Error(`Orders failed (${response.status})`);
  const orders = z
    .array(z.object({ id: z.number() }))
    .parse(await response.json());
  return <p>{orders.length} orders</p>;
}
```

In Server Functions and Route Handlers, pass `await session.writable()`
instead, the request's session that can set cookies. After a 401, it renews the token, saves it in the cookie, and sends a
`GET` again. With an HTTP client of your own, such as ky or axios, see
[Other HTTP clients](https://session-kit.pages.dev/docs/client/other-http-clients). A client without a
`fetch` option reads the token with `credentials.get()`.

To call the API from the browser too, with `createAuthFetch` or effetch's
`sessionBearer`, opt in with `clientCredentials: true` in `createServerAuth`.
The session route then serves the access token, and the client's
`credentials` get it there.

The browser keeps the access token in memory, never the refresh token; see
[Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions#the-access-token-in-the-browser).
The example app does this.

## Things to know

- **Signing out ends the session only with `revoke`.** Without it, a copy of
  the cookie made before sign-out still works: see
  [Sign-out](https://session-kit.pages.dev/docs/server-rendering/server-sessions#sign-out).
- **Writes only where Next allows them.** Use `signIn`, `signOut`,
  `updateUser` and `writable()` in Server Functions and Route Handlers only.
  In a Server Component they can't set the cookie, and `writable()` would
  refresh and then fail to save the new tokens, which spends a rotating
  refresh token.
- **One read per request.** Calls to `session` during one request read the
  cookie once, and a sign-in through `signIn` shows in `get()`.
- **Separate instances.** Next builds Proxy, Route Handlers, and pages with
  their Server Functions separately, so each has its own `serverAuth`, even
  on one server. They share no refresh. A matcher that covers every route
  that reads the session lets Proxy refresh first, so Route Handlers and
  Server Functions rarely need to.
- **A `basePath`.** Next doesn't add it to `fetch`, so the client's
  `endpoint` includes it: `"/app/api/session"`. Nor does it add it to a
  Route Handler's redirects, so a Route Handler reads the session with
  `get` and answers 401, rather than calling `require`.
- **Rendering.** Reading cookies makes a page dynamic, and the root layout
  reads the session, so every page is dynamic. Next sends such pages with
  `Cache-Control: no-store`, and plain `use cache` can't read cookies.
  Keep pages that read the session on the Node.js runtime, the default.
  Next may not pass Proxy's refreshed cookie on to Edge runtime pages.
- **Tabs.** Signing in or out in one tab reaches the others, which ask the
  server again and render their pages again. So does returning to a tab
  after a while.
- **More.** Several server instances, the cookie's size, and plain http
  during development: see
  [Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions).

## Next steps

- [Server sessions](https://session-kit.pages.dev/docs/server-rendering/server-sessions): the cookie,
  signing out, and the limits every server-rendered setup shares.
- [Using the client](https://session-kit.pages.dev/docs/client): what the browser client can do.
- [Effect on the server](https://session-kit.pages.dev/docs/effect#on-the-server): run Effects with a
  request's session.