Redis adapter bundle
WIPProduction 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:
| Class | Replaces |
|---|---|
AuthRedisSessionStore | AuthMemoryAdapter.sessions |
AuthRedisIdempotencyStore | The default in-memory idempotency cache. |
AuthRedisDPoPNonceStore | MemoryDPoPNonceStore. |
AuthRedisLimiter | AuthMemoryLimiter. |
AuthRedisEvents | AuthInMemoryEvents. |
AuthFakeRedis | A 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
| Data | Backing |
|---|---|
| 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 cache | Redis |
| DPoP nonces | Redis |
| Rate-limiter counters | Redis |
| Cross-replica events | Redis Pub/Sub |
| Orgs and memberships | SQL |
The split is not arbitrary: Redis is for volatile, write-heavy, TTL'd data; SQL is for durable, queryable, transactional data.