Skip to main content

redis adapter

Distributed key/value backend using ioredis or node-redis. Idempotent assignments via Redis sets, multi-tenant isolation via key prefix.

Install

import { IamRedisAdapter } from '@gentleduck/iam/adapters/redis'

Distributed key/value backend. Works with ioredis, node-redis v4+, or any client matching the AuthRedisLike interface.

bun add ioredis
# or
bun add redis

When to use

  • Multi-instance deploys - share policy/role state across nodes
  • Edge / serverless - Upstash Redis, Cloudflare KV (behind a AuthRedisLike shim), Vercel KV
  • Pair with Engine LRU cache - Redis = source of truth, in-process LRU = hot reads
  • Cross-tenant SaaS - keyPrefix isolates tenants in a shared Redis

Usage

ioredis

import Redis from 'ioredis'
import { IamRedisAdapter } from '@gentleduck/iam/adapters/redis'
import { IamEngine } from '@gentleduck/iam'

const redis = new Redis(process.env.REDIS_URL!)

const adapter = new IamRedisAdapter({
  client: redis,
  keyPrefix: 'iam:',
})

const engine = new IamEngine({ adapter, cacheTTL: 60 })

node-redis (v4+)

import { createClient } from 'redis'
import { IamRedisAdapter } from '@gentleduck/iam/adapters/redis'

const client = createClient({ url: process.env.REDIS_URL })
await client.connect()

const adapter = new IamRedisAdapter({ client, keyPrefix: 'iam:' })

Upstash Redis (REST)

import { Redis } from '@upstash/redis'
import { IamRedisAdapter } from '@gentleduck/iam/adapters/redis'

const redis = Redis.fromEnv()
const adapter = new IamRedisAdapter({ client: redis, keyPrefix: 'iam:' })

Upstash's client implements the same surface duck-iam needs - drop in directly.


Storage layout

Redis keyTypeContents
${prefix}policiesHashpolicyId -> JSON-encoded policy
${prefix}rolesHashroleId -> JSON-encoded role
${prefix}assignments:${subjectId}SetroleId\sscope strings (scope empty when unscoped)
${prefix}attrs:${subjectId}StringJSON-encoded attribute object

Set semantics make assignRole idempotent - calling it twice with the same (subjectId, roleId, scope) is a no-op.


Constructor config

OptionTypeDefaultDescription
clientAuthRedisLike--Any client implementing the minimal Redis surface
keyPrefixstring''Optional prefix to namespace duck-iam keys

AuthRedisLike interface

interface AuthRedisLike {
  get(key: string): Promise<string | null>
  set(key: string, value: string): Promise<unknown>
  del(...keys: string[]): Promise<number>
  hset(key: string, field: string, value: string): Promise<number>
  hget(key: string, field: string): Promise<string | null>
  hdel(key: string, ...fields: string[]): Promise<number>
  hkeys(key: string): Promise<string[]>
  hvals(key: string): Promise<string[]>
  hgetall(key: string): Promise<Record<string, string>>
  sadd(key: string, ...members: string[]): Promise<number>
  srem(key: string, ...members: string[]): Promise<number>
  smembers(key: string): Promise<string[]>
}

ioredis and node-redis v4+ both satisfy this directly. For custom clients, implement these 12 methods.


Multi-tenant isolation

Use keyPrefix to share a Redis instance across tenants without cross-talk:

const tenant1 = new IamRedisAdapter({ client, keyPrefix: 'iam:tenant1:' })
const tenant2 = new IamRedisAdapter({ client, keyPrefix: 'iam:tenant2:' })

// Same tenant, different prefix -> fully isolated
await tenant1.savePolicy({ id: 'p1', /* ... */ })
await tenant2.getPolicy('p1') // null

Pairing with Engine LRU cache

Redis adds a network hop per cache miss. Combine with Engine's in-process LRU for best of both:

const engine = new IamEngine({
  adapter: new IamRedisAdapter({ client: redis }),
  cacheTTL: 60, // 1-minute LRU TTL
  maxCacheSize: 10_000,
})

Hot reads stay in-process. After TTL expiry or engine.cache.invalidate(), the next read hits Redis. Use Redis pub/sub to broadcast engine.cache.invalidate() calls across nodes when policies change.


Notes & caveats

  • assignRole is idempotent - Redis sets dedup automatically.
  • setSubjectAttributes is read-merge-write - same race risk as Prisma/Drizzle. For high-contention attribute writes, wrap in a Lua script or use Redis WATCH/MULTI/EXEC.
  • Memory pressure - Redis stores everything in RAM. For 1000s of policies and large rule trees, monitor memory usage.
  • Persistence - configure Redis with AOF or RDB snapshots if you don't want data loss on restart.

When NOT to use

  • Single-instance apps - IamMemoryAdapter is faster and free
  • Massive policy sets (>100k rules) - relational adapters scale better with proper indexes
  • No Redis already in stack - adds infra burden; consider Postgres adapter instead

Types

All types live under the Redis namespace at @gentleduck/iam/adapters/redis. Type-only - zero bundle cost.

  • Redis.ILike - minimal Redis client surface the adapter calls (get/set/del/hset/hget/hdel/hkeys/hvals/hgetall/sadd/srem/smembers).
  • Redis.IConfig - constructor config for IamRedisAdapter (client + optional keyPrefix).
import type { Redis } from '@gentleduck/iam/adapters/redis'

const config: Redis.IConfig = {
  client: ioredis,
  keyPrefix: 'iam:',
}

Deprecated bare aliases (AuthRedisLike, RedisAdapterConfig) remain for back-compat and will be removed in 3.0.