Skip to content

Session storage

X511 stores three kinds of values during the verification flow:

KeyHoldsWritten whenTTL
session:<sessionId>Pending session marker, then the access tokenverified() fires for an unknown user; provider calls permit()pendingTtl → then 60s
uid:<sessionId> / uid:<claimId>Hashed unique identifier from a providerprovider passes a uniqueId to permit(); copied to claimId on claim60s, then mode.ttl (or 60s for one-shot)
claim:<claimId>Random claim token paired with the cookie/claim exchange succeedsmode.ttl (or 60s for one-shot)

These records live in a SessionAdapter. The default is in-process memory; production deployments should swap in an external store.

Key prefixes

Keys passed to the adapter are already namespaced by X511 (e.g. session:abc123, claim:def456, uid:abc123). Use them as-is, or add a non-colliding prefix of your own (e.g. x511:).

SessionAdapter

ts
interface SessionAdapter {
  has(key: string): Promise<boolean>
  get(key: string): Promise<string | undefined>
  set(key: string, value?: string, ttlSeconds?: number): Promise<void>
  consume(key: string): Promise<string | undefined>
}
  • has(key) — returns true if the key exists and has not expired. Used by the SSE endpoint to detect a missing or expired session before attempting to read.
  • get(key) — reads the current value (or undefined if missing/expired). The value may be undefined even when the key exists — pending sessions are stored as set(key, undefined, pendingTtl).
  • set(key, value?, ttlSeconds?) — stores or overwrites a value. value may be undefined (used to mark pending sessions). When ttlSeconds is undefined, the entry must persist until something deletes it; X511 always passes an explicit TTL.
  • consume(key) — atomic read-and-delete. Returns the value if present and removes the entry. Used at the moment of claim exchange and (in one-shot mode) on the first verified request.

Honor the TTL

X511 relies on the adapter to expire keys. If your backend ignores the ttlSeconds argument, pending sessions and verified claims will pile up indefinitely.

Atomicity of consume

For external storage backends (Redis, databases), implement consume as an atomic read-and-delete operation (e.g. Redis GETDEL, SQL DELETE … RETURNING) to prevent race conditions where two requests consume the same session.

MemorySessionAdapter

ts
class MemorySessionAdapter implements SessionAdapter

The default adapter. Stores entries in a Map and lazily evicts expired ones on read. Suitable for development and single-instance deployments.

It is unsuitable when:

  • Sessions need to survive a process restart.
  • Multiple server instances need to share state.
  • You need any kind of observability into pending claims.

To use it explicitly:

ts
import { MemorySessionAdapter } from 'x511/session'

const { verify, verified } = x511({
  // ...
  sessionAdapter: new MemorySessionAdapter(),
})

Building a Redis adapter

SessionAdapter is small enough to implement against any KV store. The Redis sketch below uses node-redis-style methods (exists, get, set, getDel):

ts
import { x511, self } from 'x511/hono'
import type { SessionAdapter } from 'x511/session'

class RedisSessionAdapter implements SessionAdapter {
  constructor(private redis: RedisClient) {}

  async has(key: string) {
    return (await this.redis.exists(key)) > 0
  }

  async get(key: string) {
    return (await this.redis.get(key)) ?? undefined
  }

  async set(key: string, value?: string, ttlSeconds?: number) {
    if (ttlSeconds !== undefined) {
      await this.redis.set(key, value ?? '', 'EX', ttlSeconds)
    } else {
      await this.redis.set(key, value ?? '')
    }
  }

  async consume(key: string) {
    const value = await this.redis.getDel(key)
    return value ?? undefined
  }
}

const { verify, verified } = x511({
  domain: 'https://your-app.example.com',
  disclosures: { minAge: 18 },
  mode: { type: 'session', ttl: 1800 },
  providers: [self({ scope: 'your-app' })],
  sessionAdapter: new RedisSessionAdapter(redis),
})

The same pattern works for SQL backends — return the deleted row from consume() using DELETE … RETURNING value.