SessionKit is a framework-agnostic, class-based cookie session engine for Node.js.
This README is the primary documentation for the open-source project. TypeDoc is kept as a backup API index.
SessionKit separates session runtime logic from framework adapter logic:
@sessionkit/core: session lifecycle, auth state, error model, pluggable store/lock contracts@sessionkit/express: Express adapter@sessionkit/hono: Hono adapter@sessionkit/redis: Redis session store and distributed lock providerpayload -> principal projectionsignIn, signOut, optionalAuth, requireAuthSessionKitErrorgetAuthInstall only the packages you need:
# core only
npm install @sessionkit/core
# express integration
npm install @sessionkit/core @sessionkit/express express
# hono integration
npm install @sessionkit/core @sessionkit/hono hono
# redis persistence
npm install @sessionkit/core @sessionkit/redis redis
This example demonstrates:
signInrequireAuthsignOutimport express from "express";
import { MapSessionStore, SessionKit } from "@sessionkit/core";
import { createExpressSessionKit } from "@sessionkit/express";
type SessionPayload = {
userId: string;
role: "user" | "admin";
};
type Principal = {
id: string;
role: "user" | "admin";
};
const coreKit = new SessionKit<SessionPayload, Principal>({
store: new MapSessionStore<SessionPayload>({
cleanupIntervalSeconds: 60,
maxSize: 10000,
}),
cookie: {
name: "sid",
path: "/",
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
},
session: {
ttlSeconds: 60 * 60 * 24,
rolling: true,
renewBeforeSeconds: 60,
},
principalFactory(payload) {
return { id: payload.userId, role: payload.role };
},
hooks: {
onUnauthorized(ctx) {
ctx.status(401);
ctx.json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
},
},
});
const sessionKit = createExpressSessionKit(coreKit);
const app = express();
app.use(express.json());
// 1) Hydrate auth context for every request.
app.use(sessionKit.middleware());
// 2) Login endpoint.
app.post("/login", async (req, res, next) => {
try {
const result = await sessionKit.signIn(
req,
res,
{ userId: "u_001", role: "user" },
{ ttlSeconds: 3600, hydrateContext: true },
);
res.json({ ok: true, sessionId: result.sessionId, expiresAt: result.expiresAt });
} catch (error) {
next(error);
}
});
// 3) Protected endpoint.
app.get(
"/me",
sessionKit.requireAuth(),
(req, res) => {
res.json({ auth: req.auth });
},
);
// 4) Logout endpoint.
app.post("/logout", async (req, res, next) => {
try {
await sessionKit.signOut(req, res, { alwaysClearCookie: true });
res.json({ ok: true });
} catch (error) {
next(error);
}
});
app.listen(3000);
This section documents all public exports from package entry points. Each subsection ends with a runnable-style example.
@sessionkit/coreSessionKit<TPayload, TPrincipal>Creates a SessionKit runtime with store, session policy, cookie policy, principal projection, and optional hooks.
const kit = new SessionKit<Payload, Principal>({
// required: session persistence implementation
store,
// required: baseline session TTL policy
session: { ttlSeconds: 3600 },
// required: map payload into principal exposed to app code
principalFactory(payload) {
return { id: payload.userId };
},
});
Creates middleware that resolves session state from cookie + store and hydrates auth context for the current request.
// core instance + adapter conversion
app.use(toExpressMiddleware(kit.middleware()));
// adapter-bound instance (recommended)
app.use(sessionKit.middleware());
Creates middleware equivalent to middleware(). This is an intent-oriented alias when authentication is optional.
// core instance + adapter conversion
app.use(toExpressMiddleware(kit.optionalAuth()));
// adapter-bound instance (recommended)
app.use(sessionKit.optionalAuth());
Creates middleware that enforces authenticated access. If the request is unauthenticated, handling priority is options.onFail, then configured hooks.onUnauthorized, then throwing SessionKitError("UNAUTHORIZED", ...).
options is optional and contains:
onFail: custom unauthenticated handler (ctx) => void | Promise<void>app.get("/private", toExpressMiddleware(kit.requireAuth()), handler);
app.get("/private", sessionKit.requireAuth(), handler);
app.get(
"/private-custom",
toExpressMiddleware(
kit.requireAuth({
// option: override unauthenticated behavior for this route
onFail(ctx) {
ctx.status(401);
ctx.json({ error: "login required" });
},
}),
),
handler,
);
Creates a new session, stores it, sets cookie, and returns SignInResult<TPrincipal>.
options is optional and contains:
ttlSeconds: per-call TTL override (default is session.ttlSeconds)hydrateContext: whether to set auth context immediately in current request (default is true)const result = await kit.signIn(
ctx,
{
// payload: your stored session data
userId: "u_001",
role: "admin",
},
{
// option: override TTL for this sign-in only
ttlSeconds: 900,
// option: immediately mark current request as authenticated
hydrateContext: true,
},
);
console.log(result.sessionId, result.principal, result.expiresAt);
Deletes session from store, clears cookie, and resets auth context to unauthenticated.
options is optional and contains:
alwaysClearCookie: if true, cookie is cleared even when store delete fails; if false, delete failure throwsawait kit.signOut(ctx, {
// option: fail hard if store deletion fails
alwaysClearCookie: false,
});
Reads auth context from request context and returns an unauthenticated default object when none is present.
const auth = kit.getAuth(ctx);
// shape includes: sessionId, session, principal, isAuthenticated
if (!auth.isAuthenticated) {
// handle guest flow
}
SessionKitOptions<TPayload, TPrincipal>Defines runtime configuration for new SessionKit(...), including required store/session/principal settings and optional cookie, token-refresh, lock, hook, and logger settings.
session options are:
rollingtouchEverySecondsrenewBeforeSecondstoken options are:
onRefreshFailconst kit = new SessionKit<Payload, Principal>({
store,
cookie: {
// option: cookie name (default: "sid")
name: "sid",
// option: cookie path (default: "/")
path: "/",
// option: cookie domain
domain: "example.com",
// option: client-side JS access (default: true means HttpOnly enabled)
httpOnly: true,
// option: secure cookie for HTTPS
secure: true,
// option: lax | strict | none (default: "lax")
sameSite: "lax",
// option: explicit max-age override in seconds
maxAgeSeconds: 3600,
},
session: {
ttlSeconds: 3600,
// option: enable rolling renewal
rolling: true,
// option: fallback renewal threshold in seconds
touchEverySeconds: 60,
// option: preferred renewal threshold in seconds
renewBeforeSeconds: 30,
},
principalFactory(payload) {
return { id: payload.userId, role: payload.role };
},
payloadTransformer(raw) {
// option: migrate/validate legacy payload shape
return raw as Payload;
},
token: {
shouldRefresh(payload, nowMs) {
return payload.accessTokenExpMs - nowMs < 60_000;
},
async refresh(payload) {
const refreshed = await refreshToken(payload.refreshToken);
return {
payload: {
...payload,
accessToken: refreshed.accessToken,
accessTokenExpMs: refreshed.expMs,
},
// option: override TTL after refresh
ttlSeconds: 3600,
};
},
// option: unauth | revoke
onRefreshFail: "revoke",
},
lockProvider,
hooks: {
onUnauthorized(ctx) {
ctx.status(401);
ctx.json({ error: "unauthorized" });
},
onInvalidSession(ctx, reason) {
console.warn("invalid session", reason);
},
},
logger: console,
});
CookieOptionsDefines cookie-level behavior used by core and adapters.
const cookieOptions = {
// option: default is "sid"
name: "sid",
// option: default is "/"
path: "/",
// option: cookie scope domain
domain: "example.com",
// option: default is true
httpOnly: true,
// option: set true for HTTPS deployment
secure: true,
// option: default is "lax"
sameSite: "strict",
// option: max-age in seconds
maxAgeSeconds: 1800,
};
HttpContextDefines the framework-neutral contract SessionKit uses to read cookies, write cookies, store auth context, and emit response status/body.
const ctx: HttpContext = {
getCookie(name) {
return null;
},
setCookie(name, value, options) {
// options includes cookie flags and optional maxAgeSeconds
},
clearCookie(name, options) {
// clears cookie using provided cookie scope
},
setAuth(value) {
// attach auth context for current request
},
getAuth() {
return null;
},
status(code) {
// set response status
},
json(body) {
// write JSON response
},
};
HttpMiddlewareDefines middleware signature accepted by adapters.
const middleware: HttpMiddleware = async (ctx, next) => {
// perform work before downstream
await next();
// perform work after downstream
};
Defines storage contracts and the bundled in-memory implementation.
SessionStore<TPayload> optional options are:
touchcloseMapSessionStore constructor options are:
cleanupIntervalSecondsmaxSizeconst memoryStore = new MapSessionStore<Payload>({
// option: cleanup interval in seconds
cleanupIntervalSeconds: 60,
// option: max entries before naive eviction
maxSize: 10_000,
});
await memoryStore.set("sid-1", { payload: { userId: "u1" }, createdAt: Date.now(), expiresAt: Date.now() + 3600_000 }, 3600);
const session = await memoryStore.get("sid-1");
await memoryStore.touch?.("sid-1", 3600);
await memoryStore.del("sid-1");
await memoryStore.close?.();
Defines distributed lock contract and the bundled no-op implementation.
const lock = new NoopLockProvider();
const result = await lock.withLock("sessionkit:refresh:sid-1", 10, async () => {
// critical section
return "ok";
});
Defines error code model, canonical error type, and helper utilities used by adapters.
try {
throw new SessionKitError("UNAUTHORIZED", "Authentication required.");
} catch (error) {
if (isSessionKitError(error)) {
const status = statusFromErrorCode(error.code);
const body = defaultErrorBody(error.code, error.message);
console.log(status, body);
}
}
Provides parser/serializer helpers used by adapters and custom integrations.
const parsed = parseCookieHeader("sid=abc123; theme=dark");
const setHeader = serializeSetCookie("sid", "abc123", {
// option: cookie path
path: "/",
// option: secure + httponly flags
secure: true,
httpOnly: true,
// option: same-site policy
sameSite: "lax",
// option: explicit max-age
maxAgeSeconds: 3600,
});
const clearHeader = serializeClearCookie("sid", { path: "/" });
@sessionkit/expresscreateExpressSessionKit(core, [options])Binds a core SessionKit instance to Express so you can use sessionKit.middleware() directly and avoid creating context manually in each handler.
const coreKit = new SessionKit<Payload, Principal>({ store, session: { ttlSeconds: 3600 }, principalFactory });
const sessionKit = createExpressSessionKit(coreKit);
app.use(sessionKit.middleware());
app.get("/private", sessionKit.requireAuth(), (req, res) => {
const auth = sessionKit.getAuth(req, res);
res.json({ me: auth.principal });
});
SessionKitExpressRequestDefines minimal request shape required by the Express adapter.
const req: SessionKitExpressRequest = {
headers: {
cookie: "sid=abc123",
},
auth: undefined,
};
SessionKitExpressResponseDefines minimal response shape required by the Express adapter.
const res: SessionKitExpressResponse = {
status(code) {
return code;
},
json(body) {
return body;
},
getHeader(name) {
return undefined;
},
setHeader(name, value) {
return value;
},
};
createExpressHttpContext(req, res)Converts Express request/response objects into core HttpContext.
const ctx = createExpressHttpContext(req, res);
await kit.signIn(ctx, { userId: "u_001", role: "user" });
toExpressMiddleware(middleware, [options])Converts core middleware to Express middleware and maps SessionKitError to HTTP responses.
options is optional and contains:
onError: custom SessionKitError handlerapp.use(
toExpressMiddleware(kit.middleware(), {
// option: customize adapter-level error output
onError(error, req, res) {
res.status(500);
res.json({ code: error.code, message: error.message });
},
}),
);
@sessionkit/honoSESSIONKIT_HONO_AUTH_KEYDefines the context key used by the Hono adapter to store auth context.
console.log(SESSIONKIT_HONO_AUTH_KEY); // "auth"
SessionKitHonoAdapterOptionsDefines adapter-level error customization for Hono integration.
options is optional and contains:
onError: custom SessionKitError handler returning Response | voidapp.use(
"*",
toHonoMiddleware(kit.middleware(), {
// option: customize error mapping
onError(error, c) {
return c.json({ code: error.code, message: error.message }, 500);
},
}),
);
createHonoSessionKit(core, [options])Binds a core SessionKit instance to Hono so handlers can use sessionKit.middleware() and sessionKit.signIn(c, ...) directly.
const coreKit = new SessionKit<Payload, Principal>({ store, session: { ttlSeconds: 3600 }, principalFactory });
const sessionKit = createHonoSessionKit(coreKit);
app.use("*", sessionKit.middleware());
app.get("/private", sessionKit.requireAuth(), (c) => {
const auth = sessionKit.getAuth(c);
return c.json({ me: auth.principal });
});
createHonoHttpContext(c)Converts Hono Context into core HttpContext.
const ctx = createHonoHttpContext(c);
const auth = kit.getAuth(ctx);
toHonoMiddleware(middleware, [options])Converts core middleware to Hono middleware and handles SessionKit error mapping.
options is optional and contains:
onError: custom SessionKitError handler returning Response | voidapp.use("/private/*", toHonoMiddleware(kit.requireAuth()));
@sessionkit/redisSessionCodec<TPayload>Defines custom serialization/deserialization strategy for persisted sessions.
const codec: SessionCodec<Payload> = {
serialize(value) {
return JSON.stringify(value);
},
deserialize(raw) {
return JSON.parse(raw) as StoredSession<Payload>;
},
};
RedisSessionStoreOptions<TPayload>Defines optional Redis store behavior.
options is optional and contains:
keyPrefixcodecconst store = new RedisSessionStore<Payload>(
{ url: "redis://localhost:6379" },
{
// option: namespacing key prefix
keyPrefix: "sessionkit:sess:",
// option: custom codec
codec,
},
);
RedisLockProviderOptionsDefines optional lock acquisition behavior.
options is optional and contains:
keyPrefixacquireTimeoutMsretryDelayMsconst lockProvider = new RedisLockProvider(
{ url: "redis://localhost:6379" },
{
// option: lock key namespace
keyPrefix: "sessionkit:lock:",
// option: max wait to acquire lock
acquireTimeoutMs: 5000,
// option: polling delay while waiting lock
retryDelayMs: 50,
},
);
RedisConnectionParamsDefines parameterized Redis connection input.
options is optional and contains:
urlhostportusernamepassworddatabasetlslazyConnectredisOptionsconst connection: RedisConnectionParams = {
// option: full URL
url: "redis://localhost:6379",
// option: enable lazy connect
lazyConnect: false,
};
RedisConnectionInputRepresents accepted constructor input for Redis store and lock provider.
const byClient: RedisConnectionInput = redisClient;
const byWrapper: RedisConnectionInput = { client: redisClient, manageClient: false, lazyConnect: true };
const byParams: RedisConnectionInput = { url: "redis://localhost:6379" };
RedisLockProviderInputRepresents accepted constructor input for RedisLockProvider.
const lockInputByParams: RedisLockProviderInput = { url: "redis://localhost:6379" };
const lockInputByStore: RedisLockProviderInput = { store };
RedisSessionStore<TPayload>Redis-backed session store implementation for SessionKit.
const store = new RedisSessionStore<Payload>({ url: "redis://localhost:6379" });
await store.set("sid-1", { payload: { userId: "u1" }, createdAt: Date.now(), expiresAt: Date.now() + 3600_000 }, 3600);
const session = await store.get("sid-1");
await store.touch("sid-1", 3600);
await store.del("sid-1");
await store.close();
RedisLockProviderRedis-backed distributed lock provider, commonly used for token refresh race control.
const lock = new RedisLockProvider({ url: "redis://localhost:6379" });
await lock.withLock("sessionkit:refresh:sid-1", 10, async () => {
// critical section work
});
await lock.close();
Recommended rollout sequence:
SessionPayload and Principal types.MapSessionStore for local development, RedisSessionStore for shared/runtime environments).httpOnly/sameSite/secure/domain/path).session.ttlSeconds and whether rolling renewal is required.middleware() and requireAuth() in routes.token.shouldRefresh, token.refresh, and token.onRefreshFail.RedisLockProvider to avoid refresh races.pnpm install
pnpm typecheck
pnpm build
pnpm test
pnpm docs:build
pnpm changeset
pnpm version-packages
pnpm release
Apache-2.0