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.redisStorage(client: redis) or
the camelCase class alias. The upstream keyPrefix: keyword is accepted
alongside the canonical Ruby key_prefix: keyword:
storage = BetterAuth.redisStorage(client: redis, keyPrefix: "my-app:")
storage = BetterAuth::RedisStorage.redisStorage(client: redis, keyPrefix: "my-app:")
Configuration
storage = BetterAuth::RedisStorage.new(
client: redis,
key_prefix: "better-auth:",
scan_count: BetterAuth::RedisStorage::SCAN_DEFAULT_COUNT,
atomic_clear: false
)
client must respond to get, set, setex, del, and scan. It should
also respond to keys only when scan_count: nil 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:". keyPrefix: is accepted for
upstream-shaped call sites. 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.
list_keys and clear escape Redis glob metacharacters in key_prefix before
matching keys, so prefixes containing characters such as *, ?, [, ], or
\ are treated as literal namespace bytes.
Warning: Passing
key_prefix: ""puts Better Auth keys at the root of the selected Redis logical namespace.list_keysandclearthen match*, so collisions across apps or tenants are possible andcleardeletes every key in that Redis database. Use an application-specific prefix unless the Redis database is fully dedicated to Better Auth.
scan_count defaults to BetterAuth::RedisStorage::SCAN_DEFAULT_COUNT and uses
Redis SCAN for list_keys and clear. Set scan_count: to a larger positive
count such as 500 or 1000 to tune scan page size:
storage = BetterAuth::RedisStorage.new(client: redis, scan_count: 500)
For exact legacy upstream behavior, pass scan_count: nil to use blocking
KEYS "#{key_prefix}*". This is intended only for small or dedicated Redis
databases because KEYS can block Redis while it walks the keyspace:
storage = BetterAuth::RedisStorage.new(client: redis, scan_count: nil)
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. The SCAN path removes duplicate cursor results while
preserving first-seen order. 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. The SCAN path collects the matched key set before
deleting it so cursor iteration is not affected by mutating the keyspace.
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 by default.
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_keysorlistKeys - 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