Skip to content

User Session Store

Sessions are a natural fit for JSON key-value entries with TTL.

import { json, kv } from "benni/schema";
type Session = {
userId: string;
createdAt: string;
};
export const sessions = kv("session", json<Session>());

Create a session:

await redis.kv(sessions).set(
sessionId,
{
userId: "42",
createdAt: new Date().toISOString()
},
{
ttlSeconds: 60 * 60 * 24 * 7
}
);

Read a session:

const session = await redis.kv(sessions).get(sessionId);

Extend a session:

await redis.kv(sessions).expire(sessionId, 60 * 60 * 24 * 7);

Delete a session:

await redis.kv(sessions).del(sessionId);

Raw Redis equivalent:

await nodeRedis.set(`session:${sessionId}`, JSON.stringify(session), {
EX: 60 * 60 * 24 * 7
});

When parts of a session expire on different schedules (say a short-lived CSRF token alongside a week-long identity), model it as a hash and give each field its own TTL. Redis 8 sets the values and their expiry atomically with HSETEX:

import { hash, number, string } from "benni/schema";
export const sessionData = hash("session", {
userId: string(),
csrfToken: string(),
lastSeen: number()
});
// Identity lives for a week; the CSRF token for an hour.
await redis.hash(sessionData).hsetex(
sessionId,
{ userId: "42", lastSeen: Date.now() },
{ ttlSeconds: 60 * 60 * 24 * 7 }
);
await redis.hash(sessionData).hsetex(
sessionId,
{ csrfToken: token },
{ ttlSeconds: 60 * 60 }
);
// Read the identity and slide its TTL in one round trip (HGETEX).
const identity = await redis.hash(sessionData).hgetex(
sessionId,
["userId", "lastSeen"],
{ ttlSeconds: 60 * 60 * 24 * 7 }
);

The CSRF token expires on its own an hour in while the identity fields keep the session alive for a week: no separate keys, and each field carries its own clock. See Field Expiration.