Overview
Some state is small, short-lived and written constantly: rate limit counters, device login codes, nonces, caches. TheAuth keeps it in a secondary storage, a tiny key/value interface with expiry. By default that is process memory, which is fine for local development and a single server. Once you run more than one instance, or on serverless, point it somewhere shared.Choosing a backend
Per-feature overrides
Pass one backend for everything, or an object. Each feature falls back todefault, then to memory.
Keys are prefixed
theauth:<feature>: automatically, so features never collide in a shared Redis or table. A misspelled feature name throws at startup.
Database adapter
"database" creates a theauth_secondary_storage table (it is added to the automatic migrations). incr is a single UPDATE counter = counter + 1 guarded by the key, so it is atomic on every supported database. Expired rows are removed lazily; for large keyspaces call databaseStorage(db).purgeExpired() from a cron.
Redis adapter
redisStorage accepts any client with get, del, incr, pexpire and pttl (node-redis spells them pExpire and pTTL, and that works too). It has no Redis dependency, so ioredis, node-redis and @upstash/redis all fit.
INCR and PEXPIRE, the next caller notices the missing TTL and repairs it, so a counter can never become permanent.
Custom store
incr, the helper builds one from get and set. That is not atomic and the result reports atomicIncr: false.
The interface
incr uses a fixed window: a missing or expired key starts at 1 with the given TTL, and later calls keep the original expiry.
Backward compatibility
kvStore(namespace), KVStore, MemoryStore and RateLimitStore keep working. KVStore now delegates to the new adapter, and it also accepts any SecondaryStorage, which gives you atomic counting through the old API. rateLimit({ store }) accepts either kind. If you set no store, the plugin follows secondaryStorage.rateLimit.