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.

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

MemberMeaning
prefixStarts 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.
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

MemberMeaning
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. 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

// 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 },
);
// src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      sessions?: Record<string, unknown>;
    }
  }
}
export {};
// 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.