Skip to main content

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

Cloudflare KV is eventually consistent and has no atomic increment. Two requests hitting different locations at the same moment can read the same count, and a write can take up to a minute to be visible elsewhere. Rate limits on KV are soft: they stop casual abuse, not a determined burst. For strict counters on Workers use "database" with D1, a Durable Object, or Upstash Redis. Plain values such as device codes work on KV if you accept the propagation delay.

Per-feature overrides

Pass one backend for everything, or an object. Each feature falls back to default, 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.
If the process dies between INCR and PEXPIRE, the next caller notices the missing TTL and repairs it, so a counter can never become permanent.

Custom store

Without 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.
Last modified on October 8, 2026