Skip to main content

Redis adapter bundle

WIP

Production session store + idempotency cache + DPoP nonce store + sliding-window limiter + cross-process events. Works against ioredis, @upstash/redis, or an in-tree AuthFakeRedis.

What it bundles

@gentleduck/auth/adapters/redis is a bundle of stores - each one can be wired independently. There is no single "Redis adapter" class; you pick the stores you need:

ClassReplaces
AuthRedisSessionStoreAuthMemoryAdapter.sessions
AuthRedisIdempotencyStoreThe default in-memory idempotency cache.
AuthRedisDPoPNonceStoreMemoryDPoPNonceStore.
AuthRedisLimiterAuthMemoryLimiter.
AuthRedisEventsAuthInMemoryEvents.
AuthFakeRedisA test-only in-memory AuthRedisLike.IClient.

Identities, credentials, and orgs do not belong in Redis - use the SQL bridge for durable rows and Redis for the volatile state that needs cross-process coordination.

Setup with ioredis

import Redis from 'ioredis'
import {
  AuthRedisDPoPNonceStore,
  AuthRedisEvents,
  AuthRedisIdempotencyStore,
  AuthRedisLimiter,
  AuthRedisSessionStore,
} from '@gentleduck/auth/adapters/redis'
import { authCreateSqlStores } from '@gentleduck/auth/adapters/sql'

const redis = new Redis(process.env.REDIS_URL!)
const sql = authCreateSqlStores({ bridge: myDrizzleBridge })

export const auth = new AuthEngine({
  stores: {
    identities: sql.identities,
    credentials: sql.credentials,
    sessions: new AuthRedisSessionStore(redis, { keyPrefix: 'sess:', ttl: 7 * 24 * 60 * 60 }),
    orgs: sql.orgs,
  },
  limiter: new AuthRedisLimiter(redis, { keyPrefix: 'ratelimit:' }),
  events: new AuthRedisEvents(redis, { channel: 'auth-events' }),
})

Setup with @upstash/redis (edge runtimes)

import { Redis as UpstashRedis } from '@upstash/redis'

const upstash = new UpstashRedis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

const auth = new AuthEngine({
  // ...
  stores: { sessions: new AuthRedisSessionStore(upstash) },
})

AuthRedisLike.IClient is the abstraction - any Redis client that exposes get, set, del, incr, expire, pttl, keys, multi, subscribe/publish works.

AuthRedisSessionStore

new AuthRedisSessionStore(client, {
  keyPrefix: 'sess:',       // default
  ttl: 7 * 24 * 60 * 60,    // default 7 days, in seconds
})

Stores serialised Session objects under keyPrefix + sessionIdHash with TTL. Revocation = DEL. Lookup = GET + JSON parse.

The session id is hashed at rest - even a Redis dump does not yield usable bearer tokens.

AuthRedisLimiter

new AuthRedisLimiter(client, {
  keyPrefix: 'ratelimit:',  // default
  slideWindow: true,        // sliding-window algorithm
})

Sliding-window rate limiter. Per-bucket atomic via MULTI + INCR + EXPIRE. Used by every provider's limiterKeyPrefix to enforce per-email / per-IP / per-key buckets.

AuthRedisIdempotencyStore

new AuthRedisIdempotencyStore(client, {
  keyPrefix: 'idempotency:',  // default
  ttl: 24 * 60 * 60,          // default 24 hours
})

Stores the result of an idempotent request keyed by the Idempotency-Key header. Subsequent requests with the same key return the cached result without re-executing.

AuthRedisDPoPNonceStore

new AuthRedisDPoPNonceStore(client, {
  ttlMs: 10 * 60 * 1000,    // default 10 minutes
})

JTI replay guard for DPoP proofs. Every accepted DPoP jti is recorded with TTL; duplicates return AUTH/DPOP_INVALID.

AuthRedisEvents

new AuthRedisEvents(client, {
  channel: 'auth-events',   // default
})

Pub/Sub-backed event bus. Publishes serialised events to the channel; every subscribed replica replays them through the same on() API.

Use cases:

  • Cache invalidation across replicas on password.changed.
  • Cross-replica presence ("how many sessions does this identity have active right now?").
  • Streaming to an audit log SaaS without a queueing layer.

AuthFakeRedis (for tests)

import { AuthFakeRedis } from '@gentleduck/auth/adapters/redis'

const redis = new AuthFakeRedis()
const limiter = new AuthRedisLimiter(redis)

// AuthFakeRedis implements the same AuthRedisLike.IClient interface as ioredis,
// but stores in-process. No Docker, no `redis-server`, no port to bind.

Useful when you want to exercise the same code paths in tests without spinning up Redis.

What goes in Redis vs SQL

DataBacking
Identities (rows that survive process restarts)SQL
Credentials (password hashes, passkey rows, API keys)SQL
Sessions (volatile, sliding TTL)Redis (or SQL for single-replica deployments)
Idempotency cacheRedis
DPoP noncesRedis
Rate-limiter countersRedis
Cross-replica eventsRedis Pub/Sub
Orgs and membershipsSQL

The split is not arbitrary: Redis is for volatile, write-heavy, TTL'd data; SQL is for durable, queryable, transactional data.