Skip to content

feat: Provide built-in KV session wrapper utilities #1034

@wangsijie

Description

@wangsijie

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions