# H3 sessions

> 

Cookie-based JWT session management for [H3](https://h3.dev) applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies, or read from an `Authorization` header.

Import paths (pick the one that matches your H3 version):

```ts
import { useJWESession, useJWSSession } from "unjwt/adapters/h3v2";
// or "unjwt/adapters/h3v1" for H3 v1 / Nuxt v4 / Nitro v2
```

## Basic usage

### Encrypted session (JWE)

```ts [basic-jwe.ts]
import { useJWESession } from "unjwt/adapters/h3v2";

app.get("/", async (event) => {
  const session = await useJWESession(event, {
    key: process.env.SESSION_SECRET!, // password, symmetric JWK, or asymmetric keypair
    maxAge: "7D",
  });

  if (!session.id) {
    // No active session yet — create one
    await session.update({ userId: "123", email: "user@example.com" });
  }

  return { user: session.data };
});
```

### Signed session (JWS)

```ts [basic-jws.ts]
import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";

const keys = await generateJWK("RS256"); // persist these!

app.get("/", async (event) => {
  const session = await useJWSSession(event, {
    key: keys,
    maxAge: "1h",
  });

  return { data: session.data };
});
```

## Configuration

### JWE — `SessionConfigJWE`

```ts
interface SessionConfigJWE<T, MaxAge, TEvent> {
  key:
    | string // password for PBES2
    | JWEEncryptJWK // symmetric or public asymmetric JWK (alg in the JWE family)
    | { privateKey: JWEAsymmetricPrivateJWK; publicKey?: JWEAsymmetricPublicJWK }; // asymmetric pair

  maxAge?: MaxAge; // ExpiresIn duration
  name?: string; // cookie name (default: "h3-jwe")
  cookie?: false | (CookieSerializeOptions & { chunkMaxLength?: number });
  sessionHeader?: false | string; // e.g. "Authorization"
  generateId?: () => string; // default: crypto.randomUUID()

  jwe?: {
    encryptOptions?: Omit<JWEEncryptOptions, "expiresIn">;
    decryptOptions?: JWTClaimValidationOptions;
  };

  hooks?: SessionHooksJWE<T, MaxAge, TEvent>;
}
```

### JWS — `SessionConfigJWS`

```ts
interface SessionConfigJWS<T, MaxAge, TEvent> {
  key:
    | JWK_oct<JWK_HMAC> // symmetric (HMAC) JWK
    | {
        privateKey: JWSAsymmetricPrivateJWK;
        publicKey: JWSAsymmetricPublicJWK | JWSAsymmetricPublicJWK[] | JWKSet;
      };

  maxAge?: MaxAge;
  name?: string; // default: "h3-jws"
  cookie?: false | (CookieSerializeOptions & { chunkMaxLength?: number });
  sessionHeader?: false | string;
  generateId?: () => string;

  jws?: {
    signOptions?: Omit<JWSSignOptions, "expiresIn">;
    verifyOptions?: JWTClaimValidationOptions;
  };

  hooks?: SessionHooksJWS<T, MaxAge, TEvent>;
}
```

## The session manager

Both adapters return the same shape:

```ts
interface SessionManager<T, ConfigMaxAge> {
  readonly id: string | undefined;
  readonly createdAt: number;
  readonly expiresAt: ConfigMaxAge extends ExpiresIn ? number : number | undefined;
  readonly data: SessionData<T>;
  readonly token: string | undefined;

  update: (update?: SessionUpdate<T>) => Promise<SessionManager<T, ConfigMaxAge>>;
  clear: () => Promise<SessionManager<T, ConfigMaxAge>>;
}

type SessionUpdate<T> =
  | Partial<SessionData<T>>
  | ((oldData: SessionData<T>) => Partial<SessionData<T>> | undefined);
```

### Lazy by design

`session.id` is `undefined` until `session.update()` is first called. This is intentional — it aligns with OAuth and spec-compliant flows where a session should only exist once a valid operation has created it. Calling `useJWESession(event, config)` alone doesn't set a cookie.

### `update()` forms

```ts [update.ts]
// Partial merge
await session.update({ theme: "dark" });

// Updater function
await session.update((oldData) => ({ count: (oldData.count ?? 0) + 1 }));

// Token rotation (new jti/iat/exp) without changing data
await session.update();
```

### `clear()` — explicit termination

```ts [clear.ts]
await session.clear();
// Session removed from event context; cookie expired in response.
```

`clear()` is semantically different from "token expired" — they trigger different hooks (`onClear` vs. `onExpire`). See [Hooks](/adapters/hooks).

## Cookies

By default:

- JWE: `path: "/", secure: true, httpOnly: true`.
- JWS: `path: "/", secure: true, httpOnly: false` (the payload is meant to be readable).

Customize via `cookie`:

```ts [cookie-opts.ts]
const session = await useJWSSession(event, {
  key: keys,
  maxAge: "1h",
  cookie: {
    sameSite: "strict",
    domain: ".example.com",
    path: "/app",
    chunkMaxLength: 3000, // split into chunks if the token exceeds this many bytes
  },
});
```

Pass `cookie: false` to disable cookie writing entirely (header-only mode — see below).

### Chunking

JWE tokens can easily exceed the 4KB cookie limit when the session payload is large. unjwt uses H3's `setChunkedCookie` to split tokens across `name.0`, `name.1`, ..., transparently reassembled on read. Default chunk size is 4000 bytes. Override via `cookie.chunkMaxLength`.

## Header-based tokens

For API clients (mobile, SPA, service-to-service) that can't use cookies, read the token from a header:

```ts [header.ts]
const session = await useJWESession(event, {
  key: secret,
  sessionHeader: "Authorization", // expects "Bearer <token>"
});
```

Custom header name:

```ts
sessionHeader: "X-Session-Token",
```

Pass `sessionHeader: false` to disable header reads (cookies only).

When both `cookie` and `sessionHeader` are enabled, the cookie takes precedence. Set `cookie: false` to force header-only behavior.

## Typing session data

```ts [typed.ts]
interface MyData {
  userId: string;
  role: "admin" | "user";
  preferences?: { theme: "dark" | "light" };
}

app.get("/", async (event) => {
  const session = await useJWESession<MyData>(event, config);
  session.data.userId; // string
  session.data.role; // "admin" | "user"
});
```

The generic threads through `update()` and all hook payloads, so type errors catch drift between your setter and getter.

## See also

- [Lifecycle hooks →](/adapters/hooks)
- [Lower-level functions →](/adapters/lower-level)
- [Example: refresh token pattern →](/examples/refresh-token-pattern)
