
# Nuxt

> Since Nuxt 4.6 a handler defined through `nuxt/server` receives the web-standard `RequestEvent` (`{ req, res, context }`) on every server builder: Nitro 2 under Nuxt 4, Nitro 3 under Nuxt 5. `unjwt/session` takes it as it is, with no `h3` import and no alias, and the same code on both.

## Handlers come from `nuxt/server`

```ts
// server/api/me.get.ts
import { defineEventHandler } from "nuxt/server";

export default defineEventHandler(async (event) => {
  const session = await requireAppSession(event);
  return { id: session.id, user: session.data.user };
});
```

The import is the whole point. Nuxt's auto-imported `defineEventHandler` is still h3's, and on Nitro 2 its handler receives the h3 v1 event, whose `req` is Node's `IncomingMessage`. Every `nuxt/server` helper refuses that event, and so does `unjwt/session`, with `ERR_SESSION_CONFIG_INVALID`:

```text
[unjwt/session] use() got an event whose "req" is not a web Request.
```

`readBody` and `createError` come from `nuxt/server` for the same reason.

## One definition, keyed from `appSecret`

```ts
// server/utils/session.ts
import { type RequestEvent, createError, deriveSecret } from "nuxt/server";
import { type SessionDefinition, type WebSessionEvent, defineJWESession } from "unjwt/session";
import { base64UrlStringify } from "unjwt/utils";

type Definition = SessionDefinition<{ user: string }, "1h", WebSessionEvent>;
let definition: Promise<Definition> | undefined;

async function define(): Promise<Definition> {
  const secret = await deriveSecret("app:session");
  const bytes = Uint8Array.from(secret.match(/../g)!, (pair) => Number.parseInt(pair, 16));
  return defineJWESession<{ user: string }, "1h">({
    key: { kty: "oct", k: base64UrlStringify(bytes), alg: "A256GCM" },
    maxAge: "1h",
    rolling: "15m",
  });
}

export async function useAppSession(event: WebSessionEvent) {
  return (definition ??= define()).then((current) => current.use(event));
}

export async function requireAppSession(event: WebSessionEvent) {
  const current = await useAppSession(event);
  if (!current.id) throw createError({ statusCode: 401, message: "Sign in first" });
  return current;
}
```

Nuxt reads `NUXT_APP_SECRET` (or `runtimeConfig.appSecret`) and generates a development one when unset; builds do not. `deriveSecret(purpose)` returns 32 HKDF-SHA256 bytes as hex, stable while `appSecret` is and distinct per purpose, which the util turns into an `A256GCM` `oct` JWK. It needs the runtime config, so the definition is created on the first call and cached in the module. A `server/utils/` export named `useSession` would clash with the `useSession` Nuxt auto-imports, hence `useAppSession`.

## A handler that throws after a write

On Nuxt 4 (Nitro 2) the prepared headers are the Node response's own, so a `Set-Cookie` written before a thrown error goes out with it: a `clear()` followed by `createError({ statusCode: 401 })` signs the client out. On Nuxt 5 (Nitro 3) Nitro's error handler answers with a `Response` of its own and the cookie is lost; see [Nitro](/frameworks/nitro#a-handler-that-throws-after-a-write).

## WebSockets

`nuxt/server` has no WebSocket API, so `defineWebSocketHandler` is h3's auto-import on both majors. The session is read once, in `upgrade`, the only hook that can refuse; the headers it returns travel on the `101` handshake, so a `Headers` passed as `res` lets the read roll the token or drop an expired cookie. What the socket needs for its lifetime goes on `peer.context`.

On Nuxt 4 the upgrade request is a plain `{ headers, url, context }` (crossws 0.3), whose `context` is the object every later hook receives:

```ts
// server/routes/ws.ts
export default defineWebSocketHandler({
  async upgrade(request) {
    const headers = new Headers();
    const session = await useAppSession({
      headers: request.headers,
      context: request.context,
      res: { headers },
    });
    if (!session.id) return new Response("Sign in first", { status: 401 });
    Object.assign(request.context, session); // a snapshot: peer.context later
    return { headers }; // a roll or a drop made during the read rides on the 101
  },
  open(peer) {
    peer.send(`hello ${(peer.context as { data: { user: string } }).data.user}`);
  },
});
```

On Nuxt 5 the upgrade request is a `Request` (crossws 0.4): the refusal is a thrown `Response` and the context goes back as a return value, `return { headers, context: { ...session } }`, as on [Nitro](/frameworks/nitro#websockets). The fields are named on both, because a spread drops a `Request`'s `headers` getter.

Nitro 2 resolves an upgrade straight to the route's hooks, past every middleware, so the `upgrade` hook is the only place a socket's cookie is read.

The `playgrounds/nuxt-4` directory of the repository is this page as a running app.
