
# Write a driver

> A driver tells the session core how one framework exposes a request: where per-request state lives, how to read a header, and how to read and write cookies. Write one and every session feature runs on that framework.

```ts
import { defineJWESession, defineSessionDriver } from "unjwt/session";

const driver = defineSessionDriver<MyEvent>({
  prefix: "[my-app]",
  state: (event) => event.locals,
  header: (event, name) => event.request.headers.get(name),
  cookies: (event) => ({
    read: (name) => event.cookies.get(name),
    write: (name, value, attrs) => event.cookies.set(name, value, attrs),
  }),
});

export const useSession = defineJWESession({ key, maxAge: "7D" }, { driver });
// useSession.use(event: MyEvent)
```

`defineSessionDriver()` returns the driver unchanged; it exists to type it. Pass the driver as `{ driver }` to `defineJWSSession()` or `defineJWESession()` from `unjwt/session`. The event type of `use()`, `token()` and every hook's `event` is then the driver's. The hook argument name belongs to the entry, not the driver: every hook from `unjwt/session` receives what you passed to `use()` as `event`, whatever driver runs it.

## `SessionDriver`

| Member | Meaning |
| --- | --- |
| `prefix` | Starts every error message the session throws, such as `"[my-app]"`. |
| `state(event)` | A record unique to the request. Session state and the per-request name registry live in it. |
| `header(event, name)` | A request header by its lowercase name, or `null`/`undefined`. |
| `cookies(event)` | The request's [`CookieJar`](#cookiejar). |
| `writable?(event)` | `false` once the response was sent, when the platform can tell: `update()` and `clear()` then throw. Absent, the response is taken as open. |
| `handed?(event)` | `{ token }` when the event carries the token itself; cookies and headers are then not read, and `update()` re-mints onto `session.token` only. |

## `CookieJar`

| Member | Meaning |
| --- | --- |
| `read(name)` | The request's cookie of that name. |
| `written?(name)` | The value this response already set for `name` (`""` once deleted). Needed only when `read` does not already see the response's writes. |
| `write?(name, value, attrs)` | Sets a cookie on the response, replacing an earlier write of the same cookie. `attrs` is a [`CookieAttributes`](/frameworks/session#options). Absent, the event takes no cookie: `update()` throws and `clear()` empties the session without one. |

Writes arrive one cookie at a time: the base cookie, then each chunk, then `Max-Age=0` for surplus chunks. One `update()` after another in the same request writes the same cookies again, so a `write` that only appends leaves the stale value in the response beside the new one. Replace the earlier `Set-Cookie` line of the same name, domain and path.

## `state(event)` is unique to the request

Session state is cached in the record `state()` returns, so a second `use()` in the same request reads it back instead of the token. That record must belong to one request and live as long as it:

- **Do** return a record stored on the request: `event.locals`, `event.context`, or a `WeakMap` keyed by the request object.
- **Do not** return a module-level object, or one shared by concurrent requests. One visitor's session would be served to the next.

## A SvelteKit driver

```ts
// src/lib/server/session.ts
import type { RequestEvent } from "@sveltejs/kit";
import {
  type CookieAttributes,
  type CookieJar,
  defineJWESession,
  defineSessionDriver,
} from "unjwt/session";

const sveltekit = defineSessionDriver<RequestEvent>({
  prefix: "[sveltekit-session]",
  state: (event) => (event.locals.sessions ??= {}),
  header: (event, name) => event.request.headers.get(name),
  cookies: (event): CookieJar<CookieAttributes> => ({
    read: (name) => event.cookies.get(name),
    write: (name, value, attrs) => {
      event.cookies.set(name, value, { ...attrs, path: attrs.path ?? "/" });
    },
  }),
});

export const appSession = defineJWESession(
  { key: JSON.parse(SESSION_KEY), name: "__Host-session", maxAge: "7D" },
  { driver: sveltekit },
);
```

```ts
// src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      sessions?: Record<string, unknown>;
    }
  }
}
export {};
```

```ts
// src/routes/me/+server.ts
export async function GET(event) {
  const session = await appSession.use(event);
  if (!session.id) return new Response(null, { status: 401 });
  return Response.json(session.data);
}
```

SvelteKit's `cookies.get()` already returns a value set earlier in the same request, so the jar needs no `written`. Its `cookies.set()` replaces an earlier write of the same cookie and throws once the response has started, which serves as the writable check.

## What the shipped drivers do

- **h3** keeps state on `event.context.sessions`, reads cookies with h3's own helpers and writes with its `setCookie`. Under Node it implements `writable` from `res.headersSent`. On h3 v2 it also copies each cookie into `res.errHeaders`, so a handler that throws still sends it.
- **Elysia** gives every resolve, guard and handler a different context object, so it keys state on the `Request` in a `WeakMap`. Its jar is Elysia's reactive cookie jar, whose values already reflect the response's writes, so it has no `written`.
- **`unjwt/session`** keeps state on `event.context.sessions` when the event has a `context`, else in a `WeakMap` keyed on the `Request` or the object passed. Its jar parses the `Cookie` header and replaces `Set-Cookie` lines on `res.headers`, and on `res.errHeaders` too when the event has one (h3 v2's error headers); it is absent on a request that only reads, and the driver implements `handed` for the `{ token }` shape.
