-
-
Notifications
You must be signed in to change notification settings - Fork 60
feat: Provide built-in KV session wrapper utilities #1034
Description
Summary
Currently, users who want to use external storage (Redis, Vercel KV, Cloudflare KV, etc.) for session data need to implement the SessionWrapper interface themselves. This is especially useful for Next.js App Router users who need token refresh to work in React Server Components (RSC), since RSC cannot write cookies but can update external storage.
Proposal
Provide a utility function createKVSessionWrapper that makes it easy to integrate with any KV-like storage:
import { createKVSessionWrapper } from '@logto/node';
// Generic interface - works with any KV storage
const sessionWrapper = createKVSessionWrapper({
get: (key) => kv.get(key),
set: (key, value, ttl) => kv.set(key, value, { ex: ttl }),
});
// Usage in Next.js
const logtoConfig = {
// ...
sessionWrapper,
};Implementation sketch
export type KVAdapter = {
get: (key: string) => Promise<string | null | undefined>;
set: (key: string, value: string, ttlSeconds?: number) => Promise<void>;
};
export type KVSessionWrapperOptions = {
keyPrefix?: string; // default: 'logto_session_'
ttl?: number; // default: 14 * 24 * 3600 (14 days)
};
export function createKVSessionWrapper(
kv: KVAdapter,
options?: KVSessionWrapperOptions
): SessionWrapper {
let currentSessionId: string | undefined;
const prefix = options?.keyPrefix ?? 'logto_session_';
const ttl = options?.ttl ?? 14 * 24 * 3600;
return {
async wrap(data: unknown): Promise<string> {
// Reuse existing session ID - critical for RSC where cookies can't be updated
const sessionId = currentSessionId ?? crypto.randomUUID();
currentSessionId = sessionId;
await kv.set(`${prefix}${sessionId}`, JSON.stringify(data), ttl);
return sessionId;
},
async unwrap(value: string): Promise<SessionData> {
if (!value) return {};
currentSessionId = value;
const data = await kv.get(`${prefix}${value}`);
return data ? JSON.parse(data) : {};
},
};
}Use cases
Vercel KV:
import { kv } from '@vercel/kv';
const sessionWrapper = createKVSessionWrapper({
get: (key) => kv.get(key),
set: (key, value, ttl) => kv.set(key, value, { ex: ttl }),
});Cloudflare KV:
const sessionWrapper = createKVSessionWrapper({
get: (key) => env.KV.get(key),
set: (key, value) => env.KV.put(key, value),
});Redis (ioredis):
const sessionWrapper = createKVSessionWrapper({
get: (key) => redis.get(key),
set: (key, value, ttl) => redis.set(key, value, 'EX', ttl),
});Why this matters
In Next.js App Router, getAccessTokenRSC() cannot persist refreshed tokens because RSC is read-only for cookies. With external storage via sessionWrapper, the session ID in the cookie stays fixed while the actual token data is updated in KV storage - making token refresh work seamlessly in RSC.
Alternatives considered
- Document the pattern in README (already done, but requires users to implement from scratch)
- Provide platform-specific wrappers (
VercelKVSessionWrapper, etc.) - more convenient but requires maintaining multiple implementations