Session Storage
Pick between database, Redis, and memory session stores.
Sessions are persisted server-side. You pick the backend on session.storageType.
The three built-in stores
| Store | SessionStorageType | Best for |
|---|---|---|
| Database | DATABASE (default) | Single-instance apps, small/medium scale, transactional consistency with the rest of your data |
| Redis | REDIS | Multi-instance apps, horizontal scale, sub-ms read latency |
| Memory | MEMORY | Local development, tests, single-instance prototypes |
Database
The default. Sessions live in the nest_auth_sessions table. Every refresh, revoke, and lookup is one SQL query.
Pros: zero extra infrastructure, joins with the rest of your data, transactional with user updates, easy to inspect during debugging.
Cons: writes scale with login/refresh traffic. On high-traffic tenants with slidingExpiration: true, the table can become a write hot-spot — switch to Redis.
Redis
Sessions live as Redis keys under your configured prefix. Refresh tokens are looked up by key — sub-millisecond on a warm cluster.
Set up:
Reusing your own Redis connection
Pass an existing ioredis client as redis.client and the library uses it instead of opening a second connection. One pool, one place to configure TLS, password, sentinel or cluster:
When client is set, the other connection fields (url, host, port, password, tls, …) are ignored; keyPrefix and ttlSeconds still apply. Lifecycle stays with your app — the library never calls quit() on a client it did not create, so shutting down Nest will not close a connection the rest of your app is still using.
The reverse also holds, as of 2.18.0: a connection the library did open is closed when the Nest application shuts down. Before that it was left open, so each app instance leaked a socket that ioredis kept trying to reconnect — noticeable as ECONNRESET noise on teardown, and as connections piling up in a process that boots more than one app (test runs, workers).
User-state cache
The guard enforces three user fields on every authenticated request: isActive, mustChangePassword and emailVerifiedAt. Reading them from the database meant one SELECT per API call for data that almost never changes — so with the Redis session store they are cached too, and a warm authenticated request costs zero database queries.
Why the default depends on your store
A stale isActive is an authentication bypass — a deactivated account would keep working until the entry expired. So the default follows whether an invalidation can actually reach every instance:
| Session store | Cache | Default | Why |
|---|---|---|---|
| Redis | shared by all instances | on | Invalidating on one instance is seen by all of them. |
| Database / Memory | per Node process | off | Deactivating on instance A would not reach instance B until the TTL expired. |
On a database or memory store you can still opt in with enabled: true — a single-instance deployment loses nothing, and a multi-instance one accepts a staleness window of ttlSeconds on the instances that did not make the write.
How entries are invalidated
Writes that go through TypeORM are handled for you. An entity subscriber on NestAuthUser drops the account's entry on every ORM insert, update or remove, so both UserService calls and a bare user.save() are covered — a deactivation takes effect on the very next request, on every instance.
Writes TypeORM cannot see are not, and must invalidate explicitly:
repository.update(...)and any other bulkUPDATE(TypeORM runs no subscriber for these)- raw SQL and migrations
- another application writing the same database
invalidateUsers(userIds) does the same for a batch. Every invalidation emits NestAuthEvents.USER_STATE_INVALIDATED with { userId }.
The TTL is a backstop for those cases, not the mechanism — it bounds how long a missed invalidation can hurt you. Lower it if you write user state outside the ORM and cannot call invalidateUser().
Never cached. Two things always go to their source: an account that does not exist (so a deleted user is refused immediately, never from a stale entry), and the session itself — that is always a session-store read, which is why a store outage still fails closed.
Set userStateCache: { enabled: false } to restore the pre-2.18.0 behaviour of reading the user row on every request.
What happens if Redis goes down
The guard fails closed. A store outage is infrastructure failure, not a failed sign-in, so it is never read as "no session":
| Response | |
|---|---|
| Route with required auth | 503 · SESSION_STORE_UNAVAILABLE |
| Route with optional auth, caller presents a token | 503 · SESSION_STORE_UNAVAILABLE |
| Route with optional auth, no token at all | normal anonymous response |
503 rather than 401 is deliberate. A 401 tells clients to sign the user out, so a brief Redis blip would log out your entire user base. 503 says retry. Handle it in your client as a transient error, not an auth failure.
The outage is logged once when it starts and once when it recovers, not once per request.
Security note. Before 2.16.0 an optional-auth route silently fell back to anonymous during a Redis outage — the request was served with no user attached. Any such route that also carries
@NestAuthRolesor@NestAuthPermissionswas effectively open for the duration of the outage. Upgrading closes that.
Pros: horizontal scale, fast, automatic key expiry, no DB write pressure, and — with the user-state cache above — no database read on an authenticated request at all.
Cons: another service to operate. If Redis goes down, authenticated requests are refused with 503 until it returns — plan for failover.
See Scaling for the full RedisSessionOptions reference and recommended settings (sentinel / cluster / retry strategy / offline queue).
Memory
Sessions live in a Map inside the Node process. Lost on restart. Lost when you scale to a second instance.
Use it for tests and local dev. Never in production, even single-instance — a deploy or crash logs every user out.
When sessions get cleaned up
| Trigger | What happens |
|---|---|
| User logs out | Their session row/key is deleted |
| User logs out of all devices | All their session rows/keys are deleted |
| Refresh token expires | Next refresh attempt errors; client logs out |
maxSessionsPerUser exceeded | Oldest session evicted on next login |
| Password reset / change | All sessions for the user are revoked |
For DB-backed sessions on long retention, run a periodic cleanup of expired rows — Redis handles this automatically via key TTL.
Custom store
The three built-ins (database, Redis, memory) cover most needs. For anything else — DynamoDB, Cloudflare KV, Mongo, a bespoke Redis-cluster wrapper — implement the SessionStore interface (or extend BaseSessionRepository for the shared expiry/helper logic) and pass it via session.store. It overrides storageType.
store accepts a ready instance or a factory for async setup:
The SessionStore interface is small (create, findById, findByUserId, findActiveByUserId, update, delete, deleteByUserId, deleteExpired, countActiveByUserId, updateLastActive). You control your store's own dependencies — nest-auth just calls these methods for every session operation.
Related
- Sessions & Tokens — the token pair and auto-refresh.
- Scaling — Redis production setup.