
# H3 sessions

> `useJWSSession()` and `useJWESession()` open the session of the current H3 request, for Nuxt, Nitro or a bare H3 app.

```ts
import { H3 } from "h3";
import { generateJWK, useJWESession } from "unjwt/adapters/h3v2";

const key = await generateJWK("A256GCM"); // load a persisted key in production

export const app = new H3()
  .post("/login", async (event) => {
    const session = await useJWESession(event, { key, maxAge: "7D" });
    await session.update({ userId: "u_123" });
    return { id: session.id }; // { id: "0f2c686c-…" }
  })
  .get("/me", async (event) => {
    const session = await useJWESession(event, { key, maxAge: "7D" });
    return { id: session.id ?? null, data: session.data }; // { id: "0f2c686c-…", data: { userId: "u_123" } }
  });
```

```http
Set-Cookie: h3-jwe=eyJraWQiOiJfNnlmWXJwNFc…; Path=/; Expires=Thu, 22 Oct 2026 12:49:03 GMT; HttpOnly; Secure; SameSite=Lax
```

## Which entry

| Import                | h3        | Runs under      |
| --------------------- | --------- | --------------- |
| `unjwt/adapters/h3v1` | `^1.15.4` | Nuxt 4, Nitro 2 |
| `unjwt/adapters/h3v2` | `^2.0.1`  | Nuxt 5, Nitro 3 |

Both entries export the same functions and types. `h3` is an optional peer dependency, so install it only if your framework does not already. Each entry also re-exports [`generateJWK`](/jwk/generate), `thumbprint`, `importPEM`, `exportPEM` and [`deriveJWKFromPassword`](/jwk/password).

## Signature

```ts
function useJWSSession<
  T extends Record<string, any> = SessionClaims,
  MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined,
>(event: H3Event, config: SessionConfigJWS<T, MaxAge>): Promise<SessionManager<T, MaxAge>>;

function useJWESession<
  T extends Record<string, any> = SessionClaims,
  MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined,
>(event: H3Event, config: SessionConfigJWE<T, MaxAge>): Promise<SessionManager<T, MaxAge>>;
```

`T` types `session.data`: `useJWSSession<{ userId: string; role: "admin" | "user" }>(event, config)`. With `maxAge` in the config, `session.expiresAt` is typed `number`.

## Options

| Option               | Type                                                    | Default                                                  | What it does                                                                                                                                                                                                                           |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`                | JWK, key pair or `CryptoKey`                            | required                                                 | The session key. See [Keys](/adapters#keys).                                                                                                                                                                                           |
| `maxAge`             | `ExpiresIn`                                             | none                                                     | Token lifetime: seconds or `"15m"`, `"1h"`, `"7D"`. Sets `exp` and the cookie's `Expires`. Without it, tokens never expire.                                                                                                            |
| `rolling`            | `true \| ExpiresIn`                                     | none                                                     | Reissues a session on read, so `exp` slides. `true` on every request, a duration once less than that remains. Needs `maxAge`. See [rolling expiry](/adapters/hooks#rolling-expiry).                                                    |
| `name`               | `string`                                                | `"h3-jws"` / `"h3-jwe"`                                  | Cookie name, and the base of the default header. Prefer `"__Host-…"`.                                                                                                                                                                  |
| `cookie`             | `false \| CookieSerializeOptions & { chunkMaxLength? }` | `{ path: "/", secure: true, sameSite: "lax", httpOnly }` | Cookie attributes. `httpOnly` defaults to `false` for JWS and `true` for JWE. `chunkMaxLength` defaults to 4000. `false` reads the header only.                                                                                        |
| `sessionHeader`      | `false \| string`                                       | `x-<name>-session`                                       | Header read when the cookie carries no token. Accepts a bare token or `Bearer <token>`.                                                                                                                                                |
| `generateId`         | `() => string`                                          | `crypto.randomUUID`                                      | Mints the `jti` of each new token.                                                                                                                                                                                                     |
| `jws.signOptions`    | `SessionSignOptions`                                    | `{}`                                                     | [`sign()`](/jwt/jws/sign) options except `expiresIn`, `expiresAt`, `notBeforeIn`, `notBeforeAt`. `alg` pins the algorithm. `protectedHeader.typ` replaces `"session+jwt"`.                                                             |
| `jws.verifyOptions`  | `SessionVerifyOptions`                                  | `{}`                                                     | [`verify()`](/jwt/jws/verify) options except `algorithms`, `typ`, `maxTokenAge`, `validateClaims`, `forceUint8Array`, `payload`. So `issuer`, `audience`, `clockTolerance`, `requiredClaims` and `allowSymmetricJWKSet` stay settable. |
| `jwe.encryptOptions` | `SessionEncryptOptions`                                 | `{}`                                                     | [`encrypt()`](/jwt/jwe/encrypt) options except the four lifetime options. `alg` and `enc` pin the algorithms.                                                                                                                          |
| `jwe.decryptOptions` | `SessionDecryptOptions`                                 | `{}`                                                     | [`decrypt()`](/jwt/jwe/decrypt) options except the ones the adapter owns. On a PBES2 session `maxIterations` defaults to the `p2c` the session mints.                                                                                  |
| `hooks`              | `SessionHooksJWS` / `SessionHooksJWE`                   | none                                                     | See [Hooks](/adapters/hooks).                                                                                                                                                                                                          |

A `jws` option only exists on `SessionConfigJWS` and a `jwe` option only on `SessionConfigJWE`.

## The session manager

| Member                                    | What it does                                                            |
| ----------------------------------------- | ----------------------------------------------------------------------- |
| `session.id`                              | The token's `jti`, or `undefined` before the first `update()`.          |
| `session.data`                            | The payload, without `jti`, `iat`, `exp` and `nbf`.                     |
| `session.createdAt` / `session.expiresAt` | The token's `iat` and `exp`, in milliseconds.                           |
| `session.token`                           | The compact token of this request.                                      |
| `session.update(data?, { maxAge? }?)`     | Issues a new token and writes the cookie. Resolves to the same manager. |
| `session.clear()`                         | Expires the cookie (and its chunks) and empties the session.            |

The members are live getters, so a manager opened before `update()` reads the new values afterwards. `update()` takes three forms:

```ts
await session.update({ role: "admin" }); // merge into data
await session.update((data) => ({ visits: (data.visits ?? 0) + 1 })); // derive from the current data
await session.update(); // rotate: new jti, iat and exp, same data
await session.update({ step: "mfa" }, { maxAge: "5m" }); // this token lives 5 minutes
```

If issuing or the cookie write fails, `update()` throws and the session rolls back to its previous state. Every JWS call (or every JWE call) with the same `name` in one request shares one session: the first call reads the token with its config, and later calls get that state back, whatever `key` or `maxAge` they pass. Two sessions with different keys need different names. A JWS and a JWE session may not share a `name` on the same request (`ERR_SESSION_CONFIG_INVALID`).

## In Nuxt and Nitro

Put the config in one place and wrap the call. In Nuxt 4 that is `server/utils/`, which Nitro auto-imports:

```ts
// server/utils/session.ts
import type { H3Event } from "h3";
import { useJWESession, type SessionConfigJWE } from "unjwt/adapters/h3v1"; // h3v2 on Nuxt 5

const config = {
  key: JSON.parse(process.env.NUXT_SESSION_KEY!), // a JWK saved from generateJWK("A256GCM")
  name: "__Host-session",
  maxAge: "7D",
} satisfies SessionConfigJWE;

export function useAppSession(event: H3Event) {
  return useJWESession<{ userId?: string }>(event, config);
}
```

```ts
// server/api/me.get.ts
export default defineEventHandler(async (event) => {
  const session = await useAppSession(event);
  if (!session.id) throw createError({ statusCode: 401 });
  return session.data;
});
```

Keep the key object created once at module scope. The adapter imports a JWK once per key object, so a JWK rebuilt on each request is imported on each request.

## Reading the token from a header

For clients that cannot keep cookies, read the token from `Authorization`:

```ts
const session = await useJWSSession(event, {
  key,
  sessionHeader: "authorization", // "Bearer <token>" or the bare token
  cookie: false, // header only; update() writes no cookie
});
```

With `cookie: false` the client needs the token some other way. Return `session.token` after `update()`, or mint one with [`signJWSSession()`](/adapters/h3/lower-level).

## After the response is sent

A response that already left cannot take a `Set-Cookie`. `update()` and `clear()` then throw `ERR_SESSION_READONLY_EVENT` and leave the session as it was:

```ts
event.node.res.end("done");
await session.update({ x: 1 });
// JWTError ERR_SESSION_READONLY_EVENT: [unjwt/h3] Cannot update session on read-only event.
```

h3 v1 checks this on every event. h3 v2 can only check it under Node (`event.runtime.node.res`). On other runtimes a write made after the response was built is accepted and never sent, which is why a session is written inside the handler, before it returns.

A read after the response was sent still works and only observes: an expired token is reported to `onExpire` without its cookie being dropped, and `rolling` does not reissue. Both happen on the next request, which carries the same token.

On h3 v2, a handler that throws after `update()` or `clear()` still sends the session cookie. The adapter copies it into `res.errHeaders`, the headers h3 sends with an error response:

```ts
await session.update({ a: 1 });
throw new HTTPError({ status: 409, message: "conflict" });
// 409, Set-Cookie: h3-jws=eyJraWQiOiIxT…
```

An app-level `onError` that returns its own `Response` replaces those headers.

## Errors

| Code                         | When                                                                                                                                                                                                                                          |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR_SESSION_KEY_INVALID`    | `key` is missing, a password string, the wrong shape, or cannot be imported.                                                                                                                                                                  |
| `ERR_SESSION_CONFIG_INVALID` | A bad `name` or `sessionHeader`, a `__Host-`/`__Secure-` name whose cookie options break the prefix, `cookie` and `sessionHeader` both `false`, an unpinned `CryptoKey`, a token over 16 chunks, or a JWS and a JWE session under one `name`. |
| `ERR_SESSION_READONLY_EVENT` | `update()` or `clear()` after the response was sent.                                                                                                                                                                                          |
| `ERR_INVALID_OPTION`         | `update()` data that is not a plain object, or a `__proto__`-like key in it.                                                                                                                                                                  |

An unreadable incoming token throws nothing. It goes to [`onError`](/adapters/hooks#onerror) as `ERR_SESSION_TOKEN_INVALID`, and an expired one to `onExpire`. All codes are listed on [Errors](/errors).

## See also

- [Lower-level functions](/adapters/h3/lower-level) for reading, minting and verifying session tokens by hand.
- [Authentication basics](/examples/authentication-basics) and the [refresh token pattern](/examples/refresh-token-pattern).
