
# Migrating to 0.9

> The session APIs deprecated during 0.8.x and removed in 0.9.0, each with the code before and after.

::warning **0.9 is not released.** This page tracks the breaking changes and deprecations planned for 0.9.0 as they land in 0.8.x releases. Expect further changes until 0.9.0 ships. ::

Everything listed here still works in 0.8.x and marks its replacement in its `@deprecated` tag. Move to the replacements while on 0.8, and the upgrade to 0.9 needs no further edits.

::note **Most apps need only this.**

1. Define each session once with `defineJWSSession(config)` or `defineJWESession(config)`, and call `.use(event)` where you called `useJWSSession(event, config)`. See [one definition per family](#sessions-one-definition-per-family).
2. A session with cookies that reads its token from `x-<name>-session` sets `sessionHeader: true`. See [the session header](#the-session-header-is-named).

::

## Sessions: one definition per family

A session is defined once, from its config, and used on every request. The definition carries the four operations that took an event and a config on every call.

```ts
// 0.8
import { useJWSSession } from "unjwt/adapters/h3";

const config = { key, maxAge: "7D" };

app.get("/me", async (event) => {
  const session = await useJWSSession(event, config);
  return session.data;
});

// 0.9
import { defineJWSSession } from "unjwt/adapters/h3";

const useSession = defineJWSSession({ key, maxAge: "7D" });

app.get("/me", async (event) => {
  const session = await useSession.use(event);
  return session.data;
});
```

A token minted outside the cookie flow, such as a magic link, comes from `issue()` and needs no request:

```ts
// 0.8
const linkConfig = { key, maxAge: "15m", cookie: false, sessionHeader: false };

const token = await signJWSSession(event, linkConfig);
const opened = await verifyJWSSession(event, linkConfig, token);

// 0.9
const magicLink = defineJWSSession({ key, maxAge: "15m", cookie: false, sessionHeader: false });

const token = await magicLink.issue({ email });
const opened = await magicLink.open(token);
```

On Elysia the plugin is unchanged. `jwsSession(config)` is `defineJWSSession(config).plugin()`, and the manager without the plugin comes from `use(context)`:

```ts
// 0.8
import { createJWSSession } from "unjwt/adapters/elysia";

const session = await createJWSSession({ cookie, request }, config);

// 0.9
import { defineJWSSession } from "unjwt/adapters/elysia";

const useSession = defineJWSSession(config);

const session = await useSession.use({ cookie, request });
```

`unjwt/adapters/h3`, `unjwt/adapters/h3v2` and `unjwt/adapters/h3v1` (the JWE functions follow the same pattern with `defineJWESession`):

| 0.8 | 0.9 |
| --- | --- |
| `useJWSSession(event, config)` | `defineJWSSession(config).use(event)` |
| `getJWSSession(event, config)` | `defineJWSSession(config).use(event)` |
| `getJWSSessionToken(event, config)` | `defineJWSSession(config).token(event)` |
| `updateJWSSession(event, config, data, o)` | `(await defineJWSSession(config).use(event)).update(data, o)` |
| `clearJWSSession(event, config)` | `(await defineJWSSession(config).use(event)).clear()` |
| `signJWSSession(event, config)` | `defineJWSSession(config).issue(data)` to mint a token, `(await defineJWSSession(config).use(event)).token` for the current one |
| `verifyJWSSession(event, config, token)` | `defineJWSSession(config).open(token)` |
| `useJWESession(event, config)` | `defineJWESession(config).use(event)` |
| `getJWESession(event, config)` | `defineJWESession(config).use(event)` |
| `getJWESessionToken(event, config)` | `defineJWESession(config).token(event)` |
| `updateJWESession(event, config, data, o)` | `(await defineJWESession(config).use(event)).update(data, o)` |
| `clearJWESession(event, config)` | `(await defineJWESession(config).use(event)).clear()` |
| `sealJWESession(event, config)` | `defineJWESession(config).issue(data)` to mint a token, `(await defineJWESession(config).use(event)).token` for the current one |
| `unsealJWESession(event, config, token)` | `defineJWESession(config).open(token)` |

`unjwt/adapters/elysia`:

| 0.8 | 0.9 |
| --- | --- |
| `createJWSSession(context, config)` | `defineJWSSession(config).use(context)` |
| `createJWESession(context, config)` | `defineJWESession(config).use(context)` |
| `jwsSession(config)` | `jwsSession(config)`, or `defineJWSSession(config).plugin()` |
| `jweSession(config)` | `jweSession(config)`, or `defineJWESession(config).plugin()` |

`getJWSSession` returned the session state and `updateJWSSession` the reissued state. The manager `use()` returns exposes the same `id`, `createdAt`, `expiresAt`, `data` and `token`, read live.

`signJWSSession` and `sealJWESession` encoded the event's current session. `issue(data)` mints a token from the data it is given, with a fresh `jti` and `iat`, and reads no request. To send the token the request already carries, read `token` from the manager.

`verifyJWSSession(event, …)` and `unsealJWESession(event, …)` handed `{ event }` (`{ context }` on Elysia) to `onVerifyKeyLookup` and `onUnsealKeyLookup`. `open(token)` takes no request, so those hooks receive the header and the config alone; a lookup that needs the request belongs with `use(event)`.

## The h3 adapters

`unjwt/adapters/h3`, `unjwt/adapters/h3v2` and `unjwt/adapters/h3v1` are removed. An h3 v2 event is the web-standard `{ req, res, context }` that `unjwt/session` takes as it is, and since Nuxt 4.6 the handlers from `nuxt/server` hand the same event on Nitro 2 and Nitro 3. One entry, no `h3` peer dependency, and Nitro apps stop depending on `h3` they never installed.

```ts
// 0.8: h3 v2, Nitro 3, Nuxt 5
import { defineJWESession } from "unjwt/adapters/h3";

// 0.9
import { type WebSessionEvent, defineJWESession } from "unjwt/session";

const session = defineJWESession({ key, maxAge: "7D" });
export const useAppSession = (event: WebSessionEvent) => session.use(event);
```

The h3 v2 adapter mirrored the session's `Set-Cookie` into `res.errHeaders`, the headers h3 sends with a thrown handler's error. `unjwt/session` does the same on its own: when the event's `res` carries an `errHeaders` `Headers`, as h3 v2's does, every cookie write and drop lands in both, so a handler that throws after `update()` or `clear()` needs no hook.

```ts
import type { H3Event } from "h3";

const session = defineJWESession<{ user: string }, "7D", H3Event>({ key, maxAge: "7D" });
```

The third generic narrows `use()` and every hook's `event` to the framework's event, as the adapter's `TEvent` did, and the hook argument keeps its name. Nitro answers errors with a `Response` of its own, which h3 sends without `errHeaders`, so there neither the adapter nor the entry makes a difference.

Two things change with the entry. The default cookie name is `unjwt-jwe` / `unjwt-jws` where the adapter used `h3-jwe` / `h3-jws`: set `name: "h3-jwe"` to keep reading the cookies already out there, or leave it and every user signs in again once, their old cookie ignored until it expires. And error messages start with `[unjwt/session]` instead of `[unjwt/h3]`; the codes are the same.

On Nuxt 4 the handler itself changes: h3's auto-imported `defineEventHandler` passes the h3 v1 event, which `unjwt/session` and every `nuxt/server` helper refuse.

```ts
// 0.8: server/utils/session.ts
import type { H3Event } from "h3";
import { type CompatEvent, defineJWESession } from "unjwt/adapters/h3v1";

const session = defineJWESession({ key, maxAge: "7D" });
export const useAppSession = (event: H3Event | CompatEvent) => session.use(event);

// 0.9: server/utils/session.ts
import type { RequestEvent } from "nuxt/server";
import { defineJWESession } from "unjwt/session";

const session = defineJWESession({ key, maxAge: "7D" });
export const useAppSession = (event: RequestEvent) => session.use(event);
```

```ts
// 0.8: server/api/me.get.ts, h3's auto-imported defineEventHandler
export default defineEventHandler(async (event) => (await useAppSession(event)).data);

// 0.9: the handler comes from nuxt/server, so the event is a RequestEvent
import { defineEventHandler } from "nuxt/server";

export default defineEventHandler(async (event) => (await useAppSession(event)).data);
```

A WebSocket `upgrade` request goes to `use()` as `{ headers: request.headers, context: request.context, res: { headers } }` on every runtime, and the `{ headers, context }` shape `CompatEvent` named is a `SessionRequestEvent` on `unjwt/session`. See [Frameworks](/frameworks) for each runtime.

## The session header is named

Left unset, `sessionHeader` lets a session with cookies read its token from `x-<name>-session` when the cookie carries none. In 0.9 that session reads cookies only unless the header is named. Set `sessionHeader: true` for `x-<name>-session`, or a header name of your own.

```ts
// 0.8: reads x-app-session, else the cookie
defineJWSSession({ name: "app", key });

// 0.9: same behaviour
defineJWSSession({ name: "app", key, sessionHeader: true });
```

A session with `cookie: false` keeps deriving `x-<name>-session` when `sessionHeader` is unset; it needs no edit.

## Deprecations

Deprecated in 0.8.2 and removed in 0.9.0. Each still works in 0.8.

| Symbol | Replacement |
| --- | --- |
| `useJWSSession`, `useJWESession` (`unjwt/adapters/h3v1`, `h3v2`) | `defineJWSSession(config).use(event)` |
| `getJWSSession`, `getJWESession` (`unjwt/adapters/h3v1`, `h3v2`) | `defineJWSSession(config).use(event)` |
| `getJWSSessionToken`, `getJWESessionToken` (`h3v1`, `h3v2`) | `defineJWSSession(config).token(event)` |
| `updateJWSSession`, `updateJWESession` (`h3v1`, `h3v2`) | `(await defineJWSSession(config).use(event)).update()` |
| `clearJWSSession`, `clearJWESession` (`h3v1`, `h3v2`) | `(await defineJWSSession(config).use(event)).clear()` |
| `signJWSSession`, `sealJWESession` (`h3v1`, `h3v2`) | `defineJWSSession(config).issue(data)` |
| `verifyJWSSession`, `unsealJWESession` (`h3v1`, `h3v2`) | `defineJWSSession(config).open(token)` |
| `createJWSSession`, `createJWESession` (`unjwt/adapters/elysia`) | `defineJWSSession(config).use(context)` |
| implicit `sessionHeader` with cookies on (`unjwt/adapters/*`) | `sessionHeader: true` |
| `unjwt/adapters/h3`, `h3v2`, `h3v1`, every export | `unjwt/session` (see [the h3 adapters](#the-h3-adapters)) |
| `exportKey` (`unjwt/jwk`) | `exportJWK`, same arguments and result |
