better_auth-redis-storage

Redis secondary storage package for Better Auth Ruby.

This gem tracks the server-side behavior of upstream @better-auth/redis-storage pinned at Better Auth v1.6.9. The Ruby gem versions independently from the upstream npm package; BetterAuth::RedisStorage::VERSION is the Ruby gem version.

Installation

Add the gem and require the package before configuring auth:

gem "better_auth-redis-storage"
require "redis"
require "better_auth/redis_storage"

redis = Redis.new(url: ENV.fetch("REDIS_URL"))

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  database: :memory,
  secondary_storage: BetterAuth.redis_storage(client: redis)
)

The canonical Ruby form is also supported:

storage = BetterAuth::RedisStorage.new(client: redis)

For upstream-shaped call sites, use BetterAuth.redis_storage(client: redis) or the camelCase class alias:

storage = BetterAuth::RedisStorage.redisStorage(client: redis)

Configuration

storage = BetterAuth::RedisStorage.new(
  client: redis,
  key_prefix: "better-auth:",
  scan_count: nil,
  atomic_clear: false
)

client must respond to get, set, setex, del, and keys. It should also respond to scan when scan_count: is configured, and to incr when atomic_clear: is enabled. This matches the interfaces exposed by the redis and redis-namespace gems.

key_prefix defaults to "better-auth:". Passing nil falls back to the default. Any other value, including the empty string, is honored verbatim. Redis databases are not isolation boundaries for shared clients; applications sharing a Redis instance should use distinct prefixes.

Warning: Passing key_prefix: "" puts Better Auth keys at the root of the selected Redis logical namespace. list_keys and clear then match *, so collisions across apps or tenants are possible and clear deletes every key in that Redis database. Use an application-specific prefix unless the Redis database is fully dedicated to Better Auth.

scan_count is a Ruby-only opt-in for large Redis databases. By default the gem uses KEYS "#{key_prefix}*" to match upstream exactly. Set scan_count: to a positive count such as 100, 500, or 1000 to use SCAN instead:

storage = BetterAuth::RedisStorage.new(client: redis, scan_count: 500)

atomic_clear is a Ruby-only opt-in for applications that need clear to be logically atomic under concurrent writers:

storage = BetterAuth::RedisStorage.new(
  client: redis,
  scan_count: 500,
  atomic_clear: true
)

When enabled, data keys are stored under a generation prefix such as better-auth:v1:<key>. Calling clear atomically increments the generation key so new reads and writes immediately move to the next generation. The previous generation is then deleted best-effort, but correctness does not depend on that physical cleanup finishing immediately.

Behavior

The storage object implements the Better Auth secondary storage contract:

storage.get(key)
storage.set(key, value, ttl = nil)
storage.delete(key)
storage.list_keys
storage.clear

listKeys is available as a camelCase alias for upstream parity.

list_keys returns every matching logical key but Redis does not guarantee key order for KEYS or SCAN. Sort the returned array in application code when a stable order matters.

TTL handling for set(key, value, ttl):

TTL value Redis command
nil, non-numeric strings, 0, negative numbers, non-finite numbers set(prefixed_key, value)
Positive Integer setex(prefixed_key, ttl, value)
Positive finite Float or other Numeric values >= 1 setex(prefixed_key, ttl.to_i, value)
Positive finite Float or other Numeric values < 1 set(prefixed_key, value)
Positive numeric String setex(prefixed_key, ttl.to_i, value)

set, delete, and clear return nil, mirroring upstream's Promise<void> contract in Ruby form. Tests and applications should assert stored values via get rather than relying on truthy return values.

clear intentionally differs from upstream when there are no matching keys: upstream calls del(...keys) even when keys is empty, while this Ruby gem skips del to avoid Redis ERR wrong number of arguments for 'del'. When keys do exist, clear deletes them in batches of BetterAuth::RedisStorage::DELETE_CHUNK_SIZE keys per del call to avoid very large Redis argument lists.

With atomic_clear: true, clear increments a generation key with Redis INCR, making old generation keys immediately invisible to get, set, delete, list_keys, and Better Auth itself. Cleanup of the old generation is best-effort and uses SCAN when scan_count: is configured.

Redis Cluster users should treat list_keys and clear as operationally constrained helpers. This adapter does not scan every cluster node, and multi-key del calls require keys to live in a compatible hash slot. Prefer a single-slot prefix strategy such as Redis hash tags when using these helpers in clustered deployments. atomic_clear: true improves the logical clear contract because correctness uses a single INCR generation key, but physical cleanup of old generations remains subject to the connected client's scan coverage.

Better Auth Usage

secondary_storage is used by Better Auth for session payload storage, active-session indexes, verification values, and rate limiting when rate_limit: { storage: "secondary-storage" } is configured.

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  database: :memory,
  secondary_storage: BetterAuth.redis_storage(client: redis),
  rate_limit: { storage: "secondary-storage", enabled: true }
)

Custom secondary storage backends should implement:

  • get(key)
  • set(key, value, ttl = nil)
  • delete(key)
  • Optional: list_keys or listKeys
  • Optional: clear

Testing

The normal unit suite skips real Redis unless explicitly enabled:

bundle exec rake test

Run the Redis integration suite with:

REDIS_INTEGRATION=1 REDIS_URL=redis://localhost:6379/15 bundle exec rake test:integration